Spaces:
Running
Running
rrrrrrrr
#4
by mmohaa - opened
- index.html +470 -24
index.html
CHANGED
|
@@ -42,6 +42,7 @@
|
|
| 42 |
border-radius:14px;outline:none;font:inherit;transition:.18s ease
|
| 43 |
}
|
| 44 |
textarea{min-height:168px;resize:vertical;padding:14px;line-height:1.52}
|
|
|
|
| 45 |
select,input{height:48px;padding:0 13px}
|
| 46 |
textarea:focus,select:focus,input:focus{border-color:rgba(139,108,255,.9);box-shadow:0 0 0 4px rgba(139,108,255,.12)}
|
| 47 |
.row{display:grid;grid-template-columns:1fr 1fr;gap:11px}
|
|
@@ -106,6 +107,11 @@
|
|
| 106 |
</select>
|
| 107 |
</div>
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
<div class="field">
|
| 110 |
<label for="prompt">Prompt</label>
|
| 111 |
<textarea id="prompt" maxlength="5000" placeholder="Describe the subject, environment, camera, lighting, mood and composition..."></textarea>
|
|
@@ -150,10 +156,12 @@
|
|
| 150 |
<div class="queuebox">
|
| 151 |
<span class="qchip" id="queuedChip">Queued: 0</span>
|
| 152 |
<span class="qchip" id="activeChip">Active: none</span>
|
|
|
|
|
|
|
| 153 |
</div>
|
| 154 |
|
| 155 |
<div class="fine">
|
| 156 |
-
This Static Space keeps
|
| 157 |
</div>
|
| 158 |
</div>
|
| 159 |
|
|
@@ -181,19 +189,146 @@
|
|
| 181 |
|
| 182 |
<script type="module">
|
| 183 |
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/+esm";
|
|
|
|
| 184 |
/*
|
| 185 |
Static frontend only.
|
| 186 |
Change this single constant if you later choose another compatible
|
| 187 |
public AIDMA/FLUX LoRA generation Space.
|
| 188 |
*/
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
const $ = id => document.getElementById(id);
|
| 192 |
const localQueue = [];
|
| 193 |
let processing = false;
|
| 194 |
let client = null;
|
| 195 |
let api = null;
|
|
|
|
|
|
|
| 196 |
let jobCounter = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
const STYLES = [
|
| 198 |
"No extra style","Photorealistic","Ultra realistic photography","Cinematic",
|
| 199 |
"Hollywood blockbuster","IMAX film still","35mm analog film","70mm epic cinema",
|
|
@@ -240,6 +375,7 @@ const STYLES = [
|
|
| 240 |
"Clean commercial","Hyper-detailed","Ethereal","Whimsical","Moody","Haunting",
|
| 241 |
"Cozy","Playful","Epic"
|
| 242 |
];
|
|
|
|
| 243 |
const STYLE_DETAIL = {
|
| 244 |
"No extra style":"",
|
| 245 |
"Photorealistic":"photorealistic, realistic materials, physically accurate lighting, natural texture",
|
|
@@ -299,28 +435,73 @@ const STYLE_DETAIL = {
|
|
| 299 |
"Cozy":"cozy atmosphere, warm practical light, tactile materials, inviting detail",
|
| 300 |
"Epic":"epic scale, heroic composition, monumental depth, dramatic visual impact"
|
| 301 |
};
|
|
|
|
| 302 |
const PROMPT_BOOST =
|
| 303 |
"masterful composition, coherent anatomy, accurate hands and facial structure, " +
|
| 304 |
"clear subject separation, refined color harmony, realistic material response, " +
|
| 305 |
"controlled detail, intentional lighting, professional visual storytelling";
|
|
|
|
| 306 |
for (const style of STYLES) {
|
| 307 |
const option = document.createElement("option");
|
| 308 |
option.value = style;
|
| 309 |
$("styles").appendChild(option);
|
| 310 |
}
|
|
|
|
| 311 |
function normalise(value) {
|
| 312 |
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
| 313 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
function updateQueueUI() {
|
| 315 |
$("queuedChip").textContent = `Queued: ${localQueue.length}`;
|
| 316 |
-
$("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
}
|
|
|
|
| 318 |
function setViewer(mode, message = "") {
|
| 319 |
$("empty").style.display = mode === "empty" ? "block" : "none";
|
| 320 |
$("loading").style.display = mode === "loading" ? "block" : "none";
|
| 321 |
$("image").style.display = mode === "image" ? "block" : "none";
|
| 322 |
if (message) $("status").textContent = message;
|
| 323 |
}
|
|
|
|
| 324 |
function endpointInfo(names) {
|
| 325 |
const named = api?.named_endpoints || {};
|
| 326 |
const entries = Object.entries(named);
|
|
@@ -334,43 +515,269 @@ function endpointInfo(names) {
|
|
| 334 |
}
|
| 335 |
return { endpoint: names[0], spec: null };
|
| 336 |
}
|
|
|
|
| 337 |
function buildPayload(spec, values, fallback) {
|
| 338 |
const params = spec?.parameters;
|
| 339 |
if (!Array.isArray(params) || !params.length) return fallback;
|
|
|
|
| 340 |
return params.map(param => {
|
| 341 |
const name = normalise(param.parameter_name || param.label || param.name || param.component);
|
| 342 |
if (Object.prototype.hasOwnProperty.call(values, name)) return values[name];
|
|
|
|
| 343 |
for (const [alias, value] of Object.entries(values)) {
|
| 344 |
if (alias && name && (name.includes(alias) || alias.includes(name))) return value;
|
| 345 |
}
|
|
|
|
| 346 |
if ("parameter_default" in param) return param.parameter_default;
|
| 347 |
return null;
|
| 348 |
});
|
| 349 |
}
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
events: ["data", "status"],
|
| 355 |
-
space_status:
|
| 356 |
-
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
}
|
| 359 |
}
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
}
|
|
|
|
| 364 |
function snapshotJob() {
|
|
|
|
| 365 |
const prompt = $("prompt").value.trim();
|
| 366 |
if (!prompt) throw new Error("Write a prompt first.");
|
|
|
|
| 367 |
const modelOption = $("model").selectedOptions[0];
|
| 368 |
const [width, height] = $("ratio").value.split("x").map(Number);
|
| 369 |
const styleName = $("style").value.trim();
|
| 370 |
const styleText = STYLE_DETAIL[styleName] ?? styleName;
|
|
|
|
| 371 |
const parts = [prompt];
|
| 372 |
if (styleText && normalise(styleName) !== normalise("No extra style")) parts.push(styleText);
|
| 373 |
if ($("boost").checked) parts.push(PROMPT_BOOST);
|
|
|
|
| 374 |
return {
|
| 375 |
id: ++jobCounter,
|
| 376 |
repo: modelOption.value,
|
|
@@ -385,44 +792,57 @@ function snapshotJob() {
|
|
| 385 |
loraScale: 0.95
|
| 386 |
};
|
| 387 |
}
|
|
|
|
| 388 |
function remoteFileUrl(value) {
|
| 389 |
if (typeof value !== "string") return null;
|
| 390 |
const text = value.trim();
|
| 391 |
if (!text) return null;
|
|
|
|
| 392 |
// Already usable in the browser.
|
| 393 |
if (/^(https?:\/\/|blob:|data:image\/)/i.test(text)) return text;
|
|
|
|
| 394 |
// Modern Gradio may return a relative file route.
|
| 395 |
-
if (text.startsWith("/gradio_api/")) return `${
|
| 396 |
-
if (text.startsWith("gradio_api/")) return `${
|
|
|
|
| 397 |
// Some Gradio versions return only the server-side temporary path.
|
| 398 |
if (
|
| 399 |
text.startsWith("/tmp/") ||
|
| 400 |
text.includes("/gradio/") ||
|
| 401 |
/\.(png|jpe?g|webp|gif|bmp)(\?|$)/i.test(text)
|
| 402 |
) {
|
| 403 |
-
return `${
|
| 404 |
}
|
|
|
|
| 405 |
return null;
|
| 406 |
}
|
|
|
|
| 407 |
function extractImageUrl(value, seen = new Set()) {
|
| 408 |
if (value == null) return null;
|
|
|
|
| 409 |
if (typeof Blob !== "undefined" && value instanceof Blob) {
|
| 410 |
return URL.createObjectURL(value);
|
| 411 |
}
|
|
|
|
| 412 |
if (typeof File !== "undefined" && value instanceof File) {
|
| 413 |
return URL.createObjectURL(value);
|
| 414 |
}
|
|
|
|
| 415 |
if (typeof value === "string") {
|
| 416 |
const trimmed = value.trim();
|
|
|
|
| 417 |
// Some component serializers return raw base64 rather than a FileData URL.
|
| 418 |
if (/^[A-Za-z0-9+/=\s]{1000,}$/.test(trimmed)) {
|
| 419 |
return `data:image/png;base64,${trimmed.replace(/\s+/g, "")}`;
|
| 420 |
}
|
|
|
|
| 421 |
return remoteFileUrl(trimmed);
|
| 422 |
}
|
|
|
|
| 423 |
if (typeof value !== "object") return null;
|
| 424 |
if (seen.has(value)) return null;
|
| 425 |
seen.add(value);
|
|
|
|
| 426 |
if (Array.isArray(value)) {
|
| 427 |
// The first output of /run_lora is the generated image.
|
| 428 |
for (const item of value) {
|
|
@@ -431,6 +851,7 @@ function extractImageUrl(value, seen = new Set()) {
|
|
| 431 |
}
|
| 432 |
return null;
|
| 433 |
}
|
|
|
|
| 434 |
// Gradio FileData commonly uses url/path/orig_name/name.
|
| 435 |
for (const key of ["url", "path", "orig_name", "name", "image", "data"]) {
|
| 436 |
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
@@ -438,12 +859,15 @@ function extractImageUrl(value, seen = new Set()) {
|
|
| 438 |
if (found) return found;
|
| 439 |
}
|
| 440 |
}
|
|
|
|
| 441 |
for (const item of Object.values(value)) {
|
| 442 |
const found = extractImageUrl(item, seen);
|
| 443 |
if (found) return found;
|
| 444 |
}
|
|
|
|
| 445 |
return null;
|
| 446 |
}
|
|
|
|
| 447 |
function showImage(url, job) {
|
| 448 |
$("image").src = url;
|
| 449 |
$("download").href = url;
|
|
@@ -451,6 +875,7 @@ function showImage(url, job) {
|
|
| 451 |
$("download").style.display = "inline-flex";
|
| 452 |
setViewer("image", `${job.modelLabel} · seed ${job.seed}`);
|
| 453 |
}
|
|
|
|
| 454 |
async function loadSelectedLora(job) {
|
| 455 |
const { endpoint, spec } = endpointInfo(["/add_custom_lora", "add_custom_lora"]);
|
| 456 |
const payload = buildPayload(
|
|
@@ -462,17 +887,20 @@ async function loadSelectedLora(job) {
|
|
| 462 |
},
|
| 463 |
[job.repo]
|
| 464 |
);
|
|
|
|
| 465 |
const result = await client.predict(endpoint, payload);
|
| 466 |
const data = result?.data || [];
|
| 467 |
const selectedIndex = Number.isFinite(Number(data[4])) ? Number(data[4]) : null;
|
| 468 |
const trigger = typeof data[5] === "string" && data[5].trim() ? data[5].trim() : job.trigger;
|
| 469 |
return { selectedIndex, trigger };
|
| 470 |
}
|
|
|
|
| 471 |
async function runGeneration(job, selectedIndex, trigger) {
|
| 472 |
const { endpoint, spec } = endpointInfo(["/run_lora", "run_lora"]);
|
| 473 |
const finalPrompt = normalise(job.prompt).includes(normalise(trigger))
|
| 474 |
? job.prompt
|
| 475 |
: `${trigger}, ${job.prompt}`;
|
|
|
|
| 476 |
const values = {
|
| 477 |
prompt: finalPrompt,
|
| 478 |
prompttext: finalPrompt,
|
|
@@ -493,6 +921,7 @@ async function runGeneration(job, selectedIndex, trigger) {
|
|
| 493 |
lorascale: job.loraScale,
|
| 494 |
adapterstrength: job.loraScale
|
| 495 |
};
|
|
|
|
| 496 |
/*
|
| 497 |
The explorer's generation function is a Python generator that emits preview
|
| 498 |
frames. Using submit() here can leave a Static frontend holding only a
|
|
@@ -503,17 +932,22 @@ async function runGeneration(job, selectedIndex, trigger) {
|
|
| 503 |
finalPrompt, null, 0.75, job.cfg, job.steps, false,
|
| 504 |
job.seed, job.width, job.height, job.loraScale
|
| 505 |
];
|
|
|
|
| 506 |
const payload = buildPayload(spec, values, fallbackWithoutState);
|
|
|
|
| 507 |
$("queueStatus").textContent = "Waiting for the remote GPU and final image";
|
| 508 |
const response = await client.predict(endpoint, payload);
|
|
|
|
| 509 |
// @gradio/client normally returns { type: "data", data: [...] }.
|
| 510 |
// Keep compatibility with clients that return the output array directly.
|
| 511 |
const outputs = response?.data ?? response;
|
| 512 |
console.log("AIDMA final Gradio outputs:", outputs);
|
|
|
|
| 513 |
// /run_lora outputs: [generated image, final seed, progress component].
|
| 514 |
const imageOutput = Array.isArray(outputs) ? outputs[0] : outputs;
|
| 515 |
const returnedSeed = Array.isArray(outputs) ? Number(outputs[1]) : NaN;
|
| 516 |
const finalUrl = extractImageUrl(imageOutput) || extractImageUrl(outputs);
|
|
|
|
| 517 |
if (!finalUrl) {
|
| 518 |
console.error("Unrecognized final Gradio image payload:", outputs);
|
| 519 |
throw new Error(
|
|
@@ -521,14 +955,17 @@ async function runGeneration(job, selectedIndex, trigger) {
|
|
| 521 |
"It may have rejected this LoRA or changed its API."
|
| 522 |
);
|
| 523 |
}
|
|
|
|
| 524 |
if (Number.isFinite(returnedSeed)) job.seed = returnedSeed;
|
| 525 |
showImage(finalUrl, job);
|
| 526 |
return finalUrl;
|
| 527 |
}
|
|
|
|
| 528 |
async function processQueue() {
|
| 529 |
if (processing || !localQueue.length) return;
|
| 530 |
processing = true;
|
| 531 |
updateQueueUI();
|
|
|
|
| 532 |
while (localQueue.length) {
|
| 533 |
const job = localQueue.shift();
|
| 534 |
updateQueueUI();
|
|
@@ -537,11 +974,9 @@ async function processQueue() {
|
|
| 537 |
$("queueStatus").textContent = "Preparing AIDMA LoRA";
|
| 538 |
$("status").className = "";
|
| 539 |
setViewer("loading", `Job #${job.id} is running.`);
|
|
|
|
| 540 |
try {
|
| 541 |
-
await
|
| 542 |
-
const loaded = await loadSelectedLora(job);
|
| 543 |
-
$("queueStatus").textContent = "Submitting to remote GPU and waiting for final output";
|
| 544 |
-
await runGeneration(job, loaded.selectedIndex, loaded.trigger);
|
| 545 |
} catch (error) {
|
| 546 |
console.error(error);
|
| 547 |
$("status").className = "error";
|
|
@@ -551,18 +986,20 @@ async function processQueue() {
|
|
| 551 |
'<div style="margin-top:8px">' + escapeHtml(error?.message || String(error)) + '</div>';
|
| 552 |
setViewer("empty");
|
| 553 |
// Force a clean connection for the next queued job.
|
| 554 |
-
|
| 555 |
-
api = null;
|
| 556 |
}
|
| 557 |
}
|
|
|
|
| 558 |
processing = false;
|
| 559 |
updateQueueUI();
|
| 560 |
}
|
|
|
|
| 561 |
function escapeHtml(value) {
|
| 562 |
return String(value).replace(/[&<>"']/g, char => ({
|
| 563 |
"&":"&","<":"<",">":">",'"':""","'":"'"
|
| 564 |
})[char]);
|
| 565 |
}
|
|
|
|
| 566 |
$("generate").addEventListener("click", () => {
|
| 567 |
try {
|
| 568 |
const job = snapshotJob();
|
|
@@ -577,13 +1014,22 @@ $("generate").addEventListener("click", () => {
|
|
| 577 |
$("prompt").focus();
|
| 578 |
}
|
| 579 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
$("prompt").addEventListener("keydown", event => {
|
| 581 |
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
| 582 |
event.preventDefault();
|
| 583 |
$("generate").click();
|
| 584 |
}
|
| 585 |
});
|
|
|
|
| 586 |
updateQueueUI();
|
| 587 |
</script>
|
| 588 |
</body>
|
| 589 |
-
</html>
|
|
|
|
| 42 |
border-radius:14px;outline:none;font:inherit;transition:.18s ease
|
| 43 |
}
|
| 44 |
textarea{min-height:168px;resize:vertical;padding:14px;line-height:1.52}
|
| 45 |
+
textarea.token-pool{min-height:108px;font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace}
|
| 46 |
select,input{height:48px;padding:0 13px}
|
| 47 |
textarea:focus,select:focus,input:focus{border-color:rgba(139,108,255,.9);box-shadow:0 0 0 4px rgba(139,108,255,.12)}
|
| 48 |
.row{display:grid;grid-template-columns:1fr 1fr;gap:11px}
|
|
|
|
| 107 |
</select>
|
| 108 |
</div>
|
| 109 |
|
| 110 |
+
<div class="field">
|
| 111 |
+
<label for="hfTokens">Hugging Face tokens · one per line · max 50</label>
|
| 112 |
+
<textarea id="hfTokens" class="token-pool" autocomplete="off" autocapitalize="off" spellcheck="false" placeholder="hf_token_1 hf_token_2 hf_token_3"></textarea>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
<div class="field">
|
| 116 |
<label for="prompt">Prompt</label>
|
| 117 |
<textarea id="prompt" maxlength="5000" placeholder="Describe the subject, environment, camera, lighting, mood and composition..."></textarea>
|
|
|
|
| 156 |
<div class="queuebox">
|
| 157 |
<span class="qchip" id="queuedChip">Queued: 0</span>
|
| 158 |
<span class="qchip" id="activeChip">Active: none</span>
|
| 159 |
+
<span class="qchip" id="tokenChip">Token: anonymous</span>
|
| 160 |
+
<span class="qchip" id="engineChip">Engines: 20</span>
|
| 161 |
</div>
|
| 162 |
|
| 163 |
<div class="fine">
|
| 164 |
+
This Static Space keeps a private FIFO queue in your browser, uses the tokens in order without pre-checking them, switches only after quota/rate-limit/authentication failure, and keeps the existing 20-engine backend failover. Tokens are kept only in this open page.
|
| 165 |
</div>
|
| 166 |
</div>
|
| 167 |
|
|
|
|
| 189 |
|
| 190 |
<script type="module">
|
| 191 |
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/+esm";
|
| 192 |
+
|
| 193 |
/*
|
| 194 |
Static frontend only.
|
| 195 |
Change this single constant if you later choose another compatible
|
| 196 |
public AIDMA/FLUX LoRA generation Space.
|
| 197 |
*/
|
| 198 |
+
/*
|
| 199 |
+
Ordered AIDMA backend failover list.
|
| 200 |
+
The current backend remains preferred until it fails, then the next one is used.
|
| 201 |
+
Add more compatible Spaces here later using the same object format.
|
| 202 |
+
*/
|
| 203 |
+
function spaceOrigin(spaceId) {
|
| 204 |
+
return `https://${spaceId.replace("/", "-").toLowerCase()}.hf.space`;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/*
|
| 208 |
+
Twenty-slot backend pool.
|
| 209 |
+
Every candidate is API-checked at runtime. Paused, broken, sleeping,
|
| 210 |
+
incompatible, or unreachable Spaces are skipped automatically.
|
| 211 |
+
*/
|
| 212 |
+
const BACKEND_SPACES = [
|
| 213 |
+
{
|
| 214 |
+
id: "multimodalart/flux-lora-the-explorer",
|
| 215 |
+
origin: spaceOrigin("multimodalart/flux-lora-the-explorer"),
|
| 216 |
+
label: "AIDMA Engine 1"
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
id: "John6666/flux-lora-the-explorer",
|
| 220 |
+
origin: spaceOrigin("John6666/flux-lora-the-explorer"),
|
| 221 |
+
label: "AIDMA Engine 2"
|
| 222 |
+
},
|
| 223 |
+
{
|
| 224 |
+
id: "Svngoku/flux-lora-the-explorer",
|
| 225 |
+
origin: spaceOrigin("Svngoku/flux-lora-the-explorer"),
|
| 226 |
+
label: "AIDMA Engine 3"
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
id: "Nymbo/flux-lora-the-explorer",
|
| 230 |
+
origin: spaceOrigin("Nymbo/flux-lora-the-explorer"),
|
| 231 |
+
label: "AIDMA Engine 4"
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
id: "Emuixom/flux-lora-the-explorer",
|
| 235 |
+
origin: spaceOrigin("Emuixom/flux-lora-the-explorer"),
|
| 236 |
+
label: "AIDMA Engine 5"
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
id: "codermert/flux-lora-the-explorer",
|
| 240 |
+
origin: spaceOrigin("codermert/flux-lora-the-explorer"),
|
| 241 |
+
label: "AIDMA Engine 6"
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
id: "reza74ii/flux-lora-the-explorer",
|
| 245 |
+
origin: spaceOrigin("reza74ii/flux-lora-the-explorer"),
|
| 246 |
+
label: "AIDMA Engine 7"
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
id: "x2778/flux-lora-the-explorer-a",
|
| 250 |
+
origin: spaceOrigin("x2778/flux-lora-the-explorer-a"),
|
| 251 |
+
label: "AIDMA Engine 8"
|
| 252 |
+
},
|
| 253 |
+
{
|
| 254 |
+
id: "Ckjdjdjf/flux-lora-the-explorer",
|
| 255 |
+
origin: spaceOrigin("Ckjdjdjf/flux-lora-the-explorer"),
|
| 256 |
+
label: "AIDMA Engine 9"
|
| 257 |
+
},
|
| 258 |
+
{
|
| 259 |
+
id: "ZENLLC/flux-lora-the-explorer",
|
| 260 |
+
origin: spaceOrigin("ZENLLC/flux-lora-the-explorer"),
|
| 261 |
+
label: "AIDMA Engine 10"
|
| 262 |
+
},
|
| 263 |
+
{
|
| 264 |
+
id: "tenet/flux-lora-the-explorer",
|
| 265 |
+
origin: spaceOrigin("tenet/flux-lora-the-explorer"),
|
| 266 |
+
label: "AIDMA Engine 11"
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
id: "seawolf2357/flxloraexp",
|
| 270 |
+
origin: spaceOrigin("seawolf2357/flxloraexp"),
|
| 271 |
+
label: "AIDMA Engine 12"
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
id: "ginipick/flxloraexp",
|
| 275 |
+
origin: spaceOrigin("ginipick/flxloraexp"),
|
| 276 |
+
label: "AIDMA Engine 13"
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
id: "John6666/flux-lora-the-explorer-crash-progress",
|
| 280 |
+
origin: spaceOrigin("John6666/flux-lora-the-explorer-crash-progress"),
|
| 281 |
+
label: "AIDMA Engine 14"
|
| 282 |
+
},
|
| 283 |
+
{
|
| 284 |
+
id: "killwithabass/flux-gay-lora-explorer",
|
| 285 |
+
origin: spaceOrigin("killwithabass/flux-gay-lora-explorer"),
|
| 286 |
+
label: "AIDMA Engine 15"
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
id: "multimodalart/flux-lora-lab",
|
| 290 |
+
origin: spaceOrigin("multimodalart/flux-lora-lab"),
|
| 291 |
+
label: "AIDMA Engine 16"
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
id: "prithivMLmods/FLUX-LoRA-DLC2",
|
| 295 |
+
origin: spaceOrigin("prithivMLmods/FLUX-LoRA-DLC2"),
|
| 296 |
+
label: "AIDMA Engine 17"
|
| 297 |
+
},
|
| 298 |
+
{
|
| 299 |
+
id: "enzostvs/lora-studio",
|
| 300 |
+
origin: spaceOrigin("enzostvs/lora-studio"),
|
| 301 |
+
label: "AIDMA Engine 18"
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
id: "ovi054/FLUX.Dev-LoRA",
|
| 305 |
+
origin: spaceOrigin("ovi054/FLUX.Dev-LoRA"),
|
| 306 |
+
label: "AIDMA Engine 19"
|
| 307 |
+
},
|
| 308 |
+
{
|
| 309 |
+
id: "waloneai/FLUX.Dev-LoRA-Serverless",
|
| 310 |
+
origin: spaceOrigin("waloneai/FLUX.Dev-LoRA-Serverless"),
|
| 311 |
+
label: "AIDMA Engine 20"
|
| 312 |
+
}
|
| 313 |
+
];
|
| 314 |
+
|
| 315 |
const $ = id => document.getElementById(id);
|
| 316 |
const localQueue = [];
|
| 317 |
let processing = false;
|
| 318 |
let client = null;
|
| 319 |
let api = null;
|
| 320 |
+
let activeBackend = null;
|
| 321 |
+
let preferredBackendIndex = 0;
|
| 322 |
let jobCounter = 0;
|
| 323 |
+
const MAX_HF_TOKENS = 50;
|
| 324 |
+
let hfTokens = [];
|
| 325 |
+
let activeTokenIndex = 0;
|
| 326 |
+
let connectedTokenIndex = -1;
|
| 327 |
+
let tokenPoolSignature = "";
|
| 328 |
+
const backendFailures = new Map();
|
| 329 |
+
const BACKEND_FAILURE_COOLDOWN_MS = 10 * 60 * 1000;
|
| 330 |
+
const BACKEND_CONNECT_TIMEOUT_MS = 18 * 1000;
|
| 331 |
+
|
| 332 |
const STYLES = [
|
| 333 |
"No extra style","Photorealistic","Ultra realistic photography","Cinematic",
|
| 334 |
"Hollywood blockbuster","IMAX film still","35mm analog film","70mm epic cinema",
|
|
|
|
| 375 |
"Clean commercial","Hyper-detailed","Ethereal","Whimsical","Moody","Haunting",
|
| 376 |
"Cozy","Playful","Epic"
|
| 377 |
];
|
| 378 |
+
|
| 379 |
const STYLE_DETAIL = {
|
| 380 |
"No extra style":"",
|
| 381 |
"Photorealistic":"photorealistic, realistic materials, physically accurate lighting, natural texture",
|
|
|
|
| 435 |
"Cozy":"cozy atmosphere, warm practical light, tactile materials, inviting detail",
|
| 436 |
"Epic":"epic scale, heroic composition, monumental depth, dramatic visual impact"
|
| 437 |
};
|
| 438 |
+
|
| 439 |
const PROMPT_BOOST =
|
| 440 |
"masterful composition, coherent anatomy, accurate hands and facial structure, " +
|
| 441 |
"clear subject separation, refined color harmony, realistic material response, " +
|
| 442 |
"controlled detail, intentional lighting, professional visual storytelling";
|
| 443 |
+
|
| 444 |
for (const style of STYLES) {
|
| 445 |
const option = document.createElement("option");
|
| 446 |
option.value = style;
|
| 447 |
$("styles").appendChild(option);
|
| 448 |
}
|
| 449 |
+
|
| 450 |
function normalise(value) {
|
| 451 |
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
| 452 |
}
|
| 453 |
+
|
| 454 |
+
function parseHfTokens(value) {
|
| 455 |
+
const unique = new Set();
|
| 456 |
+
for (const part of String(value || "").split(/[\s,;]+/)) {
|
| 457 |
+
const token = part.trim();
|
| 458 |
+
if (!token || !token.startsWith("hf_") || unique.has(token)) continue;
|
| 459 |
+
unique.add(token);
|
| 460 |
+
if (unique.size >= MAX_HF_TOKENS) break;
|
| 461 |
+
}
|
| 462 |
+
return [...unique];
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
function syncTokenPool() {
|
| 466 |
+
const nextTokens = parseHfTokens($("hfTokens").value);
|
| 467 |
+
const nextSignature = nextTokens.join("\n");
|
| 468 |
+
|
| 469 |
+
if (nextSignature !== tokenPoolSignature) {
|
| 470 |
+
hfTokens = nextTokens;
|
| 471 |
+
tokenPoolSignature = nextSignature;
|
| 472 |
+
activeTokenIndex = 0;
|
| 473 |
+
resetBackendConnection();
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
updateQueueUI();
|
| 477 |
+
return hfTokens;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
function activeToken() {
|
| 481 |
+
return hfTokens[activeTokenIndex] || "";
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
function updateQueueUI() {
|
| 485 |
$("queuedChip").textContent = `Queued: ${localQueue.length}`;
|
| 486 |
+
$("tokenChip").textContent = hfTokens.length
|
| 487 |
+
? `Token: ${activeTokenIndex + 1}/${hfTokens.length}`
|
| 488 |
+
: "Token: anonymous";
|
| 489 |
+
|
| 490 |
+
if (!processing) {
|
| 491 |
+
$("activeChip").textContent = "Active: none";
|
| 492 |
+
return;
|
| 493 |
+
}
|
| 494 |
+
const engine = activeBackend?.label || "connecting";
|
| 495 |
+
$("activeChip").textContent = `Active: ${engine}`;
|
| 496 |
}
|
| 497 |
+
|
| 498 |
function setViewer(mode, message = "") {
|
| 499 |
$("empty").style.display = mode === "empty" ? "block" : "none";
|
| 500 |
$("loading").style.display = mode === "loading" ? "block" : "none";
|
| 501 |
$("image").style.display = mode === "image" ? "block" : "none";
|
| 502 |
if (message) $("status").textContent = message;
|
| 503 |
}
|
| 504 |
+
|
| 505 |
function endpointInfo(names) {
|
| 506 |
const named = api?.named_endpoints || {};
|
| 507 |
const entries = Object.entries(named);
|
|
|
|
| 515 |
}
|
| 516 |
return { endpoint: names[0], spec: null };
|
| 517 |
}
|
| 518 |
+
|
| 519 |
function buildPayload(spec, values, fallback) {
|
| 520 |
const params = spec?.parameters;
|
| 521 |
if (!Array.isArray(params) || !params.length) return fallback;
|
| 522 |
+
|
| 523 |
return params.map(param => {
|
| 524 |
const name = normalise(param.parameter_name || param.label || param.name || param.component);
|
| 525 |
if (Object.prototype.hasOwnProperty.call(values, name)) return values[name];
|
| 526 |
+
|
| 527 |
for (const [alias, value] of Object.entries(values)) {
|
| 528 |
if (alias && name && (name.includes(alias) || alias.includes(name))) return value;
|
| 529 |
}
|
| 530 |
+
|
| 531 |
if ("parameter_default" in param) return param.parameter_default;
|
| 532 |
return null;
|
| 533 |
});
|
| 534 |
}
|
| 535 |
+
|
| 536 |
+
function withTimeout(promise, milliseconds, message) {
|
| 537 |
+
let timer;
|
| 538 |
+
const timeout = new Promise((_, reject) => {
|
| 539 |
+
timer = setTimeout(() => reject(new Error(message)), milliseconds);
|
| 540 |
+
});
|
| 541 |
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
function backendIsCoolingDown(spaceId) {
|
| 545 |
+
const failedAt = backendFailures.get(spaceId);
|
| 546 |
+
if (!failedAt) return false;
|
| 547 |
+
if (Date.now() - failedAt >= BACKEND_FAILURE_COOLDOWN_MS) {
|
| 548 |
+
backendFailures.delete(spaceId);
|
| 549 |
+
return false;
|
| 550 |
+
}
|
| 551 |
+
return true;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
function markBackendFailed(spaceId) {
|
| 555 |
+
backendFailures.set(spaceId, Date.now());
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
function backendSupportsAidmaApi(apiDescription) {
|
| 559 |
+
const named = apiDescription?.named_endpoints || {};
|
| 560 |
+
const names = Object.keys(named).map(normalise);
|
| 561 |
+
const hasAdd = names.some(name => name.includes(normalise("add_custom_lora")));
|
| 562 |
+
const hasRun = names.some(name => name.includes(normalise("run_lora")));
|
| 563 |
+
return hasAdd && hasRun;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
function resetBackendConnection() {
|
| 567 |
+
client = null;
|
| 568 |
+
api = null;
|
| 569 |
+
activeBackend = null;
|
| 570 |
+
connectedTokenIndex = -1;
|
| 571 |
+
updateQueueUI();
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
async function connectBackend(index) {
|
| 575 |
+
const backend = BACKEND_SPACES[index];
|
| 576 |
+
if (!backend) throw new Error("Unknown backend index.");
|
| 577 |
+
|
| 578 |
+
if (
|
| 579 |
+
client &&
|
| 580 |
+
activeBackend?.id === backend.id &&
|
| 581 |
+
connectedTokenIndex === activeTokenIndex
|
| 582 |
+
) return client;
|
| 583 |
+
|
| 584 |
+
resetBackendConnection();
|
| 585 |
+
activeBackend = backend;
|
| 586 |
+
updateQueueUI();
|
| 587 |
+
|
| 588 |
+
$("loadingTitle").textContent = `Connecting to ${backend.label}…`;
|
| 589 |
+
$("queueStatus").textContent = backend.id;
|
| 590 |
+
|
| 591 |
+
const handleSpaceStatus = status => {
|
| 592 |
+
if (status?.status && status.status !== "running") {
|
| 593 |
+
$("queueStatus").textContent = `${backend.label}: ${status.status}`;
|
| 594 |
+
}
|
| 595 |
+
};
|
| 596 |
+
|
| 597 |
+
const connectOptions = {
|
| 598 |
events: ["data", "status"],
|
| 599 |
+
space_status: handleSpaceStatus,
|
| 600 |
+
status_callback: handleSpaceStatus
|
| 601 |
+
};
|
| 602 |
+
const token = activeToken();
|
| 603 |
+
if (token) connectOptions.token = token;
|
| 604 |
+
|
| 605 |
+
const newClient = await withTimeout(
|
| 606 |
+
Client.connect(backend.id, connectOptions),
|
| 607 |
+
BACKEND_CONNECT_TIMEOUT_MS,
|
| 608 |
+
`${backend.label} connection timed out`
|
| 609 |
+
);
|
| 610 |
+
|
| 611 |
+
const newApi = await withTimeout(
|
| 612 |
+
newClient.view_api(),
|
| 613 |
+
BACKEND_CONNECT_TIMEOUT_MS,
|
| 614 |
+
`${backend.label} API check timed out`
|
| 615 |
+
);
|
| 616 |
+
|
| 617 |
+
if (!backendSupportsAidmaApi(newApi)) {
|
| 618 |
+
throw new Error(`${backend.label} is not compatible with the required AIDMA API`);
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
client = newClient;
|
| 622 |
+
api = newApi;
|
| 623 |
+
connectedTokenIndex = activeTokenIndex;
|
| 624 |
+
return client;
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
function errorText(error) {
|
| 628 |
+
return String(error?.message || error || "").replace(/\s+/g, " ").trim();
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
function isCallerZeroGpuQuotaError(error) {
|
| 632 |
+
const text = errorText(error).toLowerCase();
|
| 633 |
+
return (
|
| 634 |
+
text.includes("exceeded your free zerogpu quota") ||
|
| 635 |
+
text.includes("zerogpu quota") && text.includes("left") ||
|
| 636 |
+
text.includes("daily gpu quota") ||
|
| 637 |
+
text.includes("gpu quota") && text.includes("exceeded") ||
|
| 638 |
+
text.includes("try again in") && text.includes("quota")
|
| 639 |
+
);
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
function isTokenRateLimitError(error) {
|
| 643 |
+
const text = errorText(error).toLowerCase();
|
| 644 |
+
return (
|
| 645 |
+
isCallerZeroGpuQuotaError(error) ||
|
| 646 |
+
text.includes("too many requests") ||
|
| 647 |
+
text.includes("rate limit") ||
|
| 648 |
+
text.includes("rate-limit") ||
|
| 649 |
+
/(^|\D)429(\D|$)/.test(text)
|
| 650 |
+
);
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
function isTokenAuthenticationError(error) {
|
| 654 |
+
const text = errorText(error).toLowerCase();
|
| 655 |
+
return (
|
| 656 |
+
text.includes("invalid token") ||
|
| 657 |
+
text.includes("token is invalid") ||
|
| 658 |
+
text.includes("unauthorized") ||
|
| 659 |
+
text.includes("authentication failed") ||
|
| 660 |
+
text.includes("invalid credentials") ||
|
| 661 |
+
/(^|\D)401(\D|$)/.test(text)
|
| 662 |
+
);
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
function rotateToNextToken(reason) {
|
| 666 |
+
if (!hfTokens.length || activeTokenIndex + 1 >= hfTokens.length) return false;
|
| 667 |
+
|
| 668 |
+
const previousNumber = activeTokenIndex + 1;
|
| 669 |
+
activeTokenIndex += 1;
|
| 670 |
+
resetBackendConnection();
|
| 671 |
+
$("queueStatus").textContent =
|
| 672 |
+
`Token ${previousNumber} ${reason}. Switching to token ${activeTokenIndex + 1}/${hfTokens.length}…`;
|
| 673 |
+
updateQueueUI();
|
| 674 |
+
return true;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
async function generateWithBackendFailover(job) {
|
| 678 |
+
const errors = [];
|
| 679 |
+
const remainingTokenAttempts = hfTokens.length
|
| 680 |
+
? hfTokens.length - activeTokenIndex
|
| 681 |
+
: 1;
|
| 682 |
+
let attemptedTokens = 0;
|
| 683 |
+
|
| 684 |
+
while (attemptedTokens < remainingTokenAttempts) {
|
| 685 |
+
let switchedToken = false;
|
| 686 |
+
|
| 687 |
+
for (let offset = 0; offset < BACKEND_SPACES.length; offset++) {
|
| 688 |
+
const index = (preferredBackendIndex + offset) % BACKEND_SPACES.length;
|
| 689 |
+
const backend = BACKEND_SPACES[index];
|
| 690 |
+
|
| 691 |
+
if (backendIsCoolingDown(backend.id)) {
|
| 692 |
+
continue;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
try {
|
| 696 |
+
await connectBackend(index);
|
| 697 |
+
$("queueStatus").textContent = `${backend.label}: loading AIDMA LoRA`;
|
| 698 |
+
const loaded = await loadSelectedLora(job);
|
| 699 |
+
|
| 700 |
+
$("queueStatus").textContent =
|
| 701 |
+
`${backend.label}: waiting for remote GPU and final image`;
|
| 702 |
+
await runGeneration(job, loaded.selectedIndex, loaded.trigger);
|
| 703 |
+
|
| 704 |
+
preferredBackendIndex = index;
|
| 705 |
+
backendFailures.delete(backend.id);
|
| 706 |
+
return;
|
| 707 |
+
} catch (error) {
|
| 708 |
+
const message = errorText(error);
|
| 709 |
+
console.error(`${backend.label} failed:`, error);
|
| 710 |
+
|
| 711 |
+
const quotaOrRateLimit = isTokenRateLimitError(error);
|
| 712 |
+
const authenticationFailure = isTokenAuthenticationError(error);
|
| 713 |
+
|
| 714 |
+
if (hfTokens.length && (quotaOrRateLimit || authenticationFailure)) {
|
| 715 |
+
attemptedTokens += 1;
|
| 716 |
+
errors.push(`Token ${activeTokenIndex + 1}: ${message || "unavailable"}`);
|
| 717 |
+
const reason = authenticationFailure ? "was rejected" : "reached its quota/rate limit";
|
| 718 |
+
|
| 719 |
+
if (attemptedTokens < remainingTokenAttempts && rotateToNextToken(reason)) {
|
| 720 |
+
switchedToken = true;
|
| 721 |
+
break;
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
resetBackendConnection();
|
| 725 |
+
activeTokenIndex = 0;
|
| 726 |
+
updateQueueUI();
|
| 727 |
+
throw new Error(
|
| 728 |
+
"All configured Hugging Face tokens reached quota/rate-limit or were rejected. " +
|
| 729 |
+
errors.join(" | ")
|
| 730 |
+
);
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
if (!hfTokens.length && quotaOrRateLimit) {
|
| 734 |
+
resetBackendConnection();
|
| 735 |
+
throw new Error(message);
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
errors.push(`${backend.label}: ${message || "unknown error"}`);
|
| 739 |
+
markBackendFailed(backend.id);
|
| 740 |
+
resetBackendConnection();
|
| 741 |
+
|
| 742 |
+
if (offset + 1 < BACKEND_SPACES.length) {
|
| 743 |
+
let nextBackend = null;
|
| 744 |
+
for (let ahead = offset + 1; ahead < BACKEND_SPACES.length; ahead++) {
|
| 745 |
+
const nextIndex = (preferredBackendIndex + ahead) % BACKEND_SPACES.length;
|
| 746 |
+
const candidate = BACKEND_SPACES[nextIndex];
|
| 747 |
+
if (!backendIsCoolingDown(candidate.id)) {
|
| 748 |
+
nextBackend = candidate;
|
| 749 |
+
break;
|
| 750 |
+
}
|
| 751 |
+
}
|
| 752 |
+
$("queueStatus").textContent = nextBackend
|
| 753 |
+
? `${backend.label} failed. Switching to ${nextBackend.label}…`
|
| 754 |
+
: `${backend.label} failed. No healthy backend remains.`;
|
| 755 |
+
}
|
| 756 |
}
|
| 757 |
}
|
| 758 |
+
|
| 759 |
+
if (switchedToken) continue;
|
| 760 |
+
|
| 761 |
+
throw new Error(
|
| 762 |
+
"All configured AIDMA Spaces failed. " + errors.join(" | ")
|
| 763 |
+
);
|
| 764 |
+
}
|
| 765 |
}
|
| 766 |
+
|
| 767 |
function snapshotJob() {
|
| 768 |
+
syncTokenPool();
|
| 769 |
const prompt = $("prompt").value.trim();
|
| 770 |
if (!prompt) throw new Error("Write a prompt first.");
|
| 771 |
+
|
| 772 |
const modelOption = $("model").selectedOptions[0];
|
| 773 |
const [width, height] = $("ratio").value.split("x").map(Number);
|
| 774 |
const styleName = $("style").value.trim();
|
| 775 |
const styleText = STYLE_DETAIL[styleName] ?? styleName;
|
| 776 |
+
|
| 777 |
const parts = [prompt];
|
| 778 |
if (styleText && normalise(styleName) !== normalise("No extra style")) parts.push(styleText);
|
| 779 |
if ($("boost").checked) parts.push(PROMPT_BOOST);
|
| 780 |
+
|
| 781 |
return {
|
| 782 |
id: ++jobCounter,
|
| 783 |
repo: modelOption.value,
|
|
|
|
| 792 |
loraScale: 0.95
|
| 793 |
};
|
| 794 |
}
|
| 795 |
+
|
| 796 |
function remoteFileUrl(value) {
|
| 797 |
if (typeof value !== "string") return null;
|
| 798 |
const text = value.trim();
|
| 799 |
if (!text) return null;
|
| 800 |
+
|
| 801 |
// Already usable in the browser.
|
| 802 |
if (/^(https?:\/\/|blob:|data:image\/)/i.test(text)) return text;
|
| 803 |
+
|
| 804 |
// Modern Gradio may return a relative file route.
|
| 805 |
+
if (text.startsWith("/gradio_api/")) return `${activeBackend?.origin || ""}${text}`;
|
| 806 |
+
if (text.startsWith("gradio_api/")) return `${activeBackend?.origin || ""}/${text}`;
|
| 807 |
+
|
| 808 |
// Some Gradio versions return only the server-side temporary path.
|
| 809 |
if (
|
| 810 |
text.startsWith("/tmp/") ||
|
| 811 |
text.includes("/gradio/") ||
|
| 812 |
/\.(png|jpe?g|webp|gif|bmp)(\?|$)/i.test(text)
|
| 813 |
) {
|
| 814 |
+
return `${activeBackend?.origin || ""}/gradio_api/file=${encodeURI(text)}`;
|
| 815 |
}
|
| 816 |
+
|
| 817 |
return null;
|
| 818 |
}
|
| 819 |
+
|
| 820 |
function extractImageUrl(value, seen = new Set()) {
|
| 821 |
if (value == null) return null;
|
| 822 |
+
|
| 823 |
if (typeof Blob !== "undefined" && value instanceof Blob) {
|
| 824 |
return URL.createObjectURL(value);
|
| 825 |
}
|
| 826 |
+
|
| 827 |
if (typeof File !== "undefined" && value instanceof File) {
|
| 828 |
return URL.createObjectURL(value);
|
| 829 |
}
|
| 830 |
+
|
| 831 |
if (typeof value === "string") {
|
| 832 |
const trimmed = value.trim();
|
| 833 |
+
|
| 834 |
// Some component serializers return raw base64 rather than a FileData URL.
|
| 835 |
if (/^[A-Za-z0-9+/=\s]{1000,}$/.test(trimmed)) {
|
| 836 |
return `data:image/png;base64,${trimmed.replace(/\s+/g, "")}`;
|
| 837 |
}
|
| 838 |
+
|
| 839 |
return remoteFileUrl(trimmed);
|
| 840 |
}
|
| 841 |
+
|
| 842 |
if (typeof value !== "object") return null;
|
| 843 |
if (seen.has(value)) return null;
|
| 844 |
seen.add(value);
|
| 845 |
+
|
| 846 |
if (Array.isArray(value)) {
|
| 847 |
// The first output of /run_lora is the generated image.
|
| 848 |
for (const item of value) {
|
|
|
|
| 851 |
}
|
| 852 |
return null;
|
| 853 |
}
|
| 854 |
+
|
| 855 |
// Gradio FileData commonly uses url/path/orig_name/name.
|
| 856 |
for (const key of ["url", "path", "orig_name", "name", "image", "data"]) {
|
| 857 |
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
|
|
| 859 |
if (found) return found;
|
| 860 |
}
|
| 861 |
}
|
| 862 |
+
|
| 863 |
for (const item of Object.values(value)) {
|
| 864 |
const found = extractImageUrl(item, seen);
|
| 865 |
if (found) return found;
|
| 866 |
}
|
| 867 |
+
|
| 868 |
return null;
|
| 869 |
}
|
| 870 |
+
|
| 871 |
function showImage(url, job) {
|
| 872 |
$("image").src = url;
|
| 873 |
$("download").href = url;
|
|
|
|
| 875 |
$("download").style.display = "inline-flex";
|
| 876 |
setViewer("image", `${job.modelLabel} · seed ${job.seed}`);
|
| 877 |
}
|
| 878 |
+
|
| 879 |
async function loadSelectedLora(job) {
|
| 880 |
const { endpoint, spec } = endpointInfo(["/add_custom_lora", "add_custom_lora"]);
|
| 881 |
const payload = buildPayload(
|
|
|
|
| 887 |
},
|
| 888 |
[job.repo]
|
| 889 |
);
|
| 890 |
+
|
| 891 |
const result = await client.predict(endpoint, payload);
|
| 892 |
const data = result?.data || [];
|
| 893 |
const selectedIndex = Number.isFinite(Number(data[4])) ? Number(data[4]) : null;
|
| 894 |
const trigger = typeof data[5] === "string" && data[5].trim() ? data[5].trim() : job.trigger;
|
| 895 |
return { selectedIndex, trigger };
|
| 896 |
}
|
| 897 |
+
|
| 898 |
async function runGeneration(job, selectedIndex, trigger) {
|
| 899 |
const { endpoint, spec } = endpointInfo(["/run_lora", "run_lora"]);
|
| 900 |
const finalPrompt = normalise(job.prompt).includes(normalise(trigger))
|
| 901 |
? job.prompt
|
| 902 |
: `${trigger}, ${job.prompt}`;
|
| 903 |
+
|
| 904 |
const values = {
|
| 905 |
prompt: finalPrompt,
|
| 906 |
prompttext: finalPrompt,
|
|
|
|
| 921 |
lorascale: job.loraScale,
|
| 922 |
adapterstrength: job.loraScale
|
| 923 |
};
|
| 924 |
+
|
| 925 |
/*
|
| 926 |
The explorer's generation function is a Python generator that emits preview
|
| 927 |
frames. Using submit() here can leave a Static frontend holding only a
|
|
|
|
| 932 |
finalPrompt, null, 0.75, job.cfg, job.steps, false,
|
| 933 |
job.seed, job.width, job.height, job.loraScale
|
| 934 |
];
|
| 935 |
+
|
| 936 |
const payload = buildPayload(spec, values, fallbackWithoutState);
|
| 937 |
+
|
| 938 |
$("queueStatus").textContent = "Waiting for the remote GPU and final image";
|
| 939 |
const response = await client.predict(endpoint, payload);
|
| 940 |
+
|
| 941 |
// @gradio/client normally returns { type: "data", data: [...] }.
|
| 942 |
// Keep compatibility with clients that return the output array directly.
|
| 943 |
const outputs = response?.data ?? response;
|
| 944 |
console.log("AIDMA final Gradio outputs:", outputs);
|
| 945 |
+
|
| 946 |
// /run_lora outputs: [generated image, final seed, progress component].
|
| 947 |
const imageOutput = Array.isArray(outputs) ? outputs[0] : outputs;
|
| 948 |
const returnedSeed = Array.isArray(outputs) ? Number(outputs[1]) : NaN;
|
| 949 |
const finalUrl = extractImageUrl(imageOutput) || extractImageUrl(outputs);
|
| 950 |
+
|
| 951 |
if (!finalUrl) {
|
| 952 |
console.error("Unrecognized final Gradio image payload:", outputs);
|
| 953 |
throw new Error(
|
|
|
|
| 955 |
"It may have rejected this LoRA or changed its API."
|
| 956 |
);
|
| 957 |
}
|
| 958 |
+
|
| 959 |
if (Number.isFinite(returnedSeed)) job.seed = returnedSeed;
|
| 960 |
showImage(finalUrl, job);
|
| 961 |
return finalUrl;
|
| 962 |
}
|
| 963 |
+
|
| 964 |
async function processQueue() {
|
| 965 |
if (processing || !localQueue.length) return;
|
| 966 |
processing = true;
|
| 967 |
updateQueueUI();
|
| 968 |
+
|
| 969 |
while (localQueue.length) {
|
| 970 |
const job = localQueue.shift();
|
| 971 |
updateQueueUI();
|
|
|
|
| 974 |
$("queueStatus").textContent = "Preparing AIDMA LoRA";
|
| 975 |
$("status").className = "";
|
| 976 |
setViewer("loading", `Job #${job.id} is running.`);
|
| 977 |
+
|
| 978 |
try {
|
| 979 |
+
await generateWithBackendFailover(job);
|
|
|
|
|
|
|
|
|
|
| 980 |
} catch (error) {
|
| 981 |
console.error(error);
|
| 982 |
$("status").className = "error";
|
|
|
|
| 986 |
'<div style="margin-top:8px">' + escapeHtml(error?.message || String(error)) + '</div>';
|
| 987 |
setViewer("empty");
|
| 988 |
// Force a clean connection for the next queued job.
|
| 989 |
+
resetBackendConnection();
|
|
|
|
| 990 |
}
|
| 991 |
}
|
| 992 |
+
|
| 993 |
processing = false;
|
| 994 |
updateQueueUI();
|
| 995 |
}
|
| 996 |
+
|
| 997 |
function escapeHtml(value) {
|
| 998 |
return String(value).replace(/[&<>"']/g, char => ({
|
| 999 |
"&":"&","<":"<",">":">",'"':""","'":"'"
|
| 1000 |
})[char]);
|
| 1001 |
}
|
| 1002 |
+
|
| 1003 |
$("generate").addEventListener("click", () => {
|
| 1004 |
try {
|
| 1005 |
const job = snapshotJob();
|
|
|
|
| 1014 |
$("prompt").focus();
|
| 1015 |
}
|
| 1016 |
});
|
| 1017 |
+
|
| 1018 |
+
$("hfTokens").addEventListener("input", () => {
|
| 1019 |
+
const preview = parseHfTokens($("hfTokens").value);
|
| 1020 |
+
$("tokenChip").textContent = preview.length
|
| 1021 |
+
? `Tokens entered: ${preview.length}/${MAX_HF_TOKENS}`
|
| 1022 |
+
: "Token: anonymous";
|
| 1023 |
+
});
|
| 1024 |
+
|
| 1025 |
$("prompt").addEventListener("keydown", event => {
|
| 1026 |
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
| 1027 |
event.preventDefault();
|
| 1028 |
$("generate").click();
|
| 1029 |
}
|
| 1030 |
});
|
| 1031 |
+
|
| 1032 |
updateQueueUI();
|
| 1033 |
</script>
|
| 1034 |
</body>
|
| 1035 |
+
</html>
|