Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <meta name="color-scheme" content="dark" /> | |
| <title>AIDMA Studio</title> | |
| <style> | |
| :root{ | |
| --bg:#070910;--panel:#111522;--panel2:#0b0e18;--line:rgba(255,255,255,.09); | |
| --text:#f6f7fb;--muted:#9aa3b8;--purple:#8b6cff;--mint:#4ce6ce;--red:#ff6c82; | |
| --shadow:0 30px 90px rgba(0,0,0,.42) | |
| } | |
| *{box-sizing:border-box} | |
| body{ | |
| margin:0;min-height:100vh;color:var(--text); | |
| font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif; | |
| background: | |
| radial-gradient(circle at 12% 5%,rgba(139,108,255,.20),transparent 29%), | |
| radial-gradient(circle at 90% 12%,rgba(76,230,206,.13),transparent 25%), | |
| linear-gradient(180deg,#0b0e18 0%,var(--bg) 62%); | |
| } | |
| .shell{width:min(1160px,calc(100% - 28px));margin:auto;padding:28px 0 45px} | |
| header{display:flex;align-items:center;justify-content:space-between;gap:18px;margin-bottom:22px} | |
| .brand{display:flex;align-items:center;gap:13px} | |
| .logo{ | |
| width:48px;height:48px;border-radius:16px;display:grid;place-items:center; | |
| font-weight:950;color:#060910;background:linear-gradient(135deg,var(--purple),var(--mint)); | |
| box-shadow:0 16px 38px rgba(139,108,255,.28) | |
| } | |
| h1{font-size:clamp(25px,4vw,37px);letter-spacing:-1.3px;margin:0} | |
| .sub{font-size:13px;color:var(--muted);margin-top:4px} | |
| .tag{font-size:12px;color:#cfd4e4;border:1px solid var(--line);border-radius:999px;padding:8px 11px;background:rgba(255,255,255,.035)} | |
| .grid{display:grid;grid-template-columns:minmax(0,.85fr) minmax(0,1.15fr);gap:19px} | |
| .panel{background:rgba(17,21,34,.83);border:1px solid var(--line);border-radius:24px;box-shadow:var(--shadow);backdrop-filter:blur(18px)} | |
| .controls{padding:21px} | |
| .viewer{padding:13px;min-height:625px;display:flex;flex-direction:column} | |
| label{display:block;margin:0 0 8px;font-size:11px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:#bcc3d4} | |
| .field{margin-bottom:16px} | |
| textarea,select,input{ | |
| width:100%;border:1px solid var(--line);background:var(--panel2);color:var(--text); | |
| border-radius:14px;outline:none;font:inherit;transition:.18s ease | |
| } | |
| textarea{min-height:168px;resize:vertical;padding:14px;line-height:1.52} | |
| select,input{height:48px;padding:0 13px} | |
| textarea:focus,select:focus,input:focus{border-color:rgba(139,108,255,.9);box-shadow:0 0 0 4px rgba(139,108,255,.12)} | |
| .row{display:grid;grid-template-columns:1fr 1fr;gap:11px} | |
| .boost{display:flex;justify-content:space-between;align-items:center;gap:12px;margin:0 1px 17px;color:#d4d9e7;font-size:13px} | |
| .boost input{width:18px;height:18px;accent-color:var(--purple)} | |
| button{ | |
| width:100%;height:53px;border:0;border-radius:16px;cursor:pointer;color:#07100e; | |
| font:850 15px Inter,system-ui;background:linear-gradient(135deg,var(--purple),var(--mint)); | |
| box-shadow:0 17px 38px rgba(139,108,255,.24);transition:.18s ease | |
| } | |
| button:hover{transform:translateY(-1px);filter:brightness(1.05)} | |
| button:disabled{cursor:not-allowed;opacity:.58;transform:none} | |
| .fine{font-size:11px;line-height:1.5;color:#757f95;margin:13px 2px 0} | |
| .canvas{ | |
| position:relative;display:grid;place-items:center;flex:1;min-height:520px;overflow:hidden; | |
| border-radius:18px;border:1px solid var(--line); | |
| background: | |
| linear-gradient(45deg,rgba(255,255,255,.026) 25%,transparent 25%,transparent 75%,rgba(255,255,255,.026) 75%), | |
| linear-gradient(45deg,rgba(255,255,255,.026) 25%,transparent 25%,transparent 75%,rgba(255,255,255,.026) 75%),#090c15; | |
| background-position:0 0,14px 14px;background-size:28px 28px | |
| } | |
| #image{display:none;width:100%;height:100%;object-fit:contain;background:#080a10} | |
| .empty,.loading{text-align:center;color:var(--muted);padding:30px;max-width:470px} | |
| .orb{width:73px;height:73px;margin:0 auto 17px;border-radius:25px;display:grid;place-items:center;font-size:29px;background:linear-gradient(135deg,rgba(139,108,255,.28),rgba(76,230,206,.19));border:1px solid rgba(255,255,255,.1)} | |
| .loading{display:none} | |
| .spinner{width:43px;height:43px;margin:0 auto 14px;border-radius:50%;border:3px solid rgba(255,255,255,.1);border-top-color:var(--mint);animation:spin .8s linear infinite} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| .bar{display:flex;align-items:center;justify-content:space-between;gap:13px;padding:13px 4px 1px;min-height:48px;color:var(--muted);font-size:13px} | |
| #status{overflow-wrap:anywhere} | |
| #download{display:none;text-decoration:none;color:#06110e;background:var(--mint);border-radius:11px;padding:9px 13px;font-weight:850;white-space:nowrap} | |
| .queuebox{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px} | |
| .qchip{font-size:11px;border:1px solid var(--line);border-radius:999px;padding:7px 10px;color:#bcc4d5;background:rgba(255,255,255,.025)} | |
| .error{color:var(--red)!important} | |
| @media(max-width:900px){.grid{grid-template-columns:1fr}.viewer{min-height:520px}header{align-items:flex-start;flex-direction:column}} | |
| @media(max-width:520px){.shell{width:calc(100% - 18px);padding-top:17px}.panel{border-radius:19px}.controls{padding:16px}.viewer{padding:9px}.row{grid-template-columns:1fr}} | |
| </style> | |
| </head> | |
| <body> | |
| <main class="shell"> | |
| <header> | |
| <div class="brand"> | |
| <div class="logo">A6</div> | |
| <div> | |
| <h1>AIDMA Studio</h1> | |
| <div class="sub">Static HTML interface · AIDMA-only remote generation</div> | |
| </div> | |
| </div> | |
| <div class="tag">Fresh automatic seed every time</div> | |
| </header> | |
| <section class="grid"> | |
| <div class="panel controls"> | |
| <div class="field"> | |
| <label for="model">AIDMA model</label> | |
| <select id="model"> | |
| <option value="Keltezaa/midjourney-v6-1-meets-flux-sdxl" data-trigger="(aidmaMJ6.1)"> | |
| AIDMA MJ6.1 · FLUX v0.4 | |
| </option> | |
| <option value="demo001s/MidjourneyMeetsFlux" data-trigger="aidmaMJ6.1"> | |
| AIDMA MJ6.1 · FLUX v0.5 Experimental | |
| </option> | |
| </select> | |
| </div> | |
| <div class="field"> | |
| <label for="prompt">Prompt</label> | |
| <textarea id="prompt" maxlength="5000" placeholder="Describe the subject, environment, camera, lighting, mood and composition..."></textarea> | |
| </div> | |
| <div class="field"> | |
| <label for="style">Style</label> | |
| <input id="style" list="styles" value="Cinematic" maxlength="300" placeholder="Choose or type any style" /> | |
| <datalist id="styles"></datalist> | |
| </div> | |
| <div class="row"> | |
| <div class="field"> | |
| <label for="ratio">Aspect ratio</label> | |
| <select id="ratio"> | |
| <option value="1024x1024">1:1</option> | |
| <option value="1280x720" selected>16:9</option> | |
| <option value="720x1280">9:16</option> | |
| <option value="1152x864">4:3</option> | |
| <option value="864x1152">3:4</option> | |
| <option value="1216x832">3:2</option> | |
| <option value="832x1216">2:3</option> | |
| </select> | |
| </div> | |
| <div class="field"> | |
| <label for="quality">Quality</label> | |
| <select id="quality"> | |
| <option value="20">Fast</option> | |
| <option value="28" selected>Balanced</option> | |
| <option value="40">Ultra</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="boost"> | |
| <span>Professional prompt boost</span> | |
| <input id="boost" type="checkbox" checked /> | |
| </div> | |
| <button id="generate">Add to private browser queue</button> | |
| <div class="queuebox"> | |
| <span class="qchip" id="queuedChip">Queued: 0</span> | |
| <span class="qchip" id="activeChip">Active: none</span> | |
| </div> | |
| <div class="fine"> | |
| This Static Space keeps its own FIFO queue inside your browser tab. The remote GPU is provided by the connected public Hugging Face generation Space. | |
| </div> | |
| </div> | |
| <div class="panel viewer"> | |
| <div class="canvas"> | |
| <div class="empty" id="empty"> | |
| <div class="orb">✦</div> | |
| <strong>Your next frame begins here.</strong> | |
| <div style="margin-top:8px">Write a prompt and add it to your queue.</div> | |
| </div> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <strong id="loadingTitle">Connecting to the AIDMA engine…</strong> | |
| <div id="queueStatus" style="margin-top:8px">Preparing request</div> | |
| </div> | |
| <img id="image" alt="Generated image" /> | |
| </div> | |
| <div class="bar"> | |
| <div id="status">Ready.</div> | |
| <a id="download" download="aidma-image.png">Download</a> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/+esm"; | |
| /* | |
| Static frontend only. | |
| Change this single constant if you later choose another compatible | |
| public AIDMA/FLUX LoRA generation Space. | |
| */ | |
| const BACKEND_SPACE = "multimodalart/flux-lora-the-explorer"; | |
| const BACKEND_ORIGIN = "https://multimodalart-flux-lora-the-explorer.hf.space"; | |
| const $ = id => document.getElementById(id); | |
| const localQueue = []; | |
| let processing = false; | |
| let client = null; | |
| let api = null; | |
| let jobCounter = 0; | |
| const STYLES = [ | |
| "No extra style","Photorealistic","Ultra realistic photography","Cinematic", | |
| "Hollywood blockbuster","IMAX film still","35mm analog film","70mm epic cinema", | |
| "Documentary photography","Fashion editorial","Luxury fashion campaign", | |
| "Studio portrait","Environmental portrait","Street photography","Sports photography", | |
| "Wildlife photography","Macro photography","Aerial photography","Drone photography", | |
| "Underwater photography","Astrophotography","Architectural photography", | |
| "Interior design photography","Product photography","Luxury product advertising", | |
| "Food photography","Automotive photography","Concert photography","Wedding photography", | |
| "Photojournalism","Vintage photography","Polaroid","Kodachrome","Technicolor", | |
| "Film noir","Neo-noir","Black and white fine art","High-key lighting","Low-key lighting", | |
| "Golden hour","Blue hour","Soft diffused light","Dramatic chiaroscuro", | |
| "Volumetric lighting","Neon lighting","Bioluminescent lighting","Cyberpunk", | |
| "Solarpunk","Steampunk","Dieselpunk","Atompunk","Cassette futurism","Retro futurism", | |
| "Y2K futurism","Sci-fi concept art","Space opera","Hard science fiction","Alien world", | |
| "Dystopian","Utopian","Post-apocalyptic","Fantasy concept art","Dark fantasy", | |
| "High fantasy","Epic fantasy","Mythological","Medieval","Renaissance","Baroque", | |
| "Rococo","Victorian","Art Nouveau","Art Deco","Bauhaus","Brutalism","Minimalism", | |
| "Maximalism","Surrealism","Dreamlike surrealism","Magical realism","Expressionism", | |
| "Impressionism","Post-impressionism","Romanticism","Symbolism","Cubism","Futurism", | |
| "Dada","Pop art","Op art","Abstract art","Geometric abstraction","Psychedelic art", | |
| "Visionary art","Graffiti","Street art","Ink illustration","Pen and ink", | |
| "Graphite drawing","Charcoal drawing","Colored pencil","Pastel drawing","Watercolor", | |
| "Gouache","Oil painting","Acrylic painting","Impasto painting","Digital painting", | |
| "Matte painting","Concept art","Key art","Book cover illustration", | |
| "Editorial illustration","Children's book illustration","Comic book art", | |
| "Graphic novel","Manga","Anime","90s anime","Retro anime","Cel animation", | |
| "Stop motion","Claymation","Paper cutout","Origami art","Low-poly 3D", | |
| "Stylized 3D","Photorealistic 3D render","Unreal Engine cinematic","Octane render", | |
| "Ray-traced CGI","Isometric 3D","Voxel art","Pixel art","16-bit game art", | |
| "8-bit game art","PS1 game aesthetic","Retro arcade","Game concept art", | |
| "Fantasy game splash art","Trading card art","Holographic art","Chrome metallic", | |
| "Liquid metal","Crystal sculpture","Porcelain sculpture","Marble sculpture", | |
| "Bronze sculpture","Wood carving","Embroidery","Tapestry","Mosaic","Stained glass", | |
| "Paper collage","Mixed media","Blueprint","Technical drawing","Scientific illustration", | |
| "Botanical illustration","Anatomical illustration","Logo design","Poster design", | |
| "Album cover","Magazine cover","Movie poster","Luxury branding","Minimalist advertising", | |
| "Japanese ukiyo-e","Chinese ink wash","Persian miniature","Islamic geometric art", | |
| "Ancient Egyptian mural","Greek pottery art","Roman mosaic","Celtic art", | |
| "Nordic folk art","Mexican folk art","Indian miniature painting","Arabesque ornament", | |
| "Gothic cathedral art","Dark academia","Light academia","Cottagecore","Fairycore", | |
| "Royalcore","Dreamcore","Weirdcore","Liminal space","Vaporwave","Synthwave", | |
| "Retrowave","Lo-fi aesthetic","Grunge","Heavy metal album art","Elegant luxury", | |
| "Clean commercial","Hyper-detailed","Ethereal","Whimsical","Moody","Haunting", | |
| "Cozy","Playful","Epic" | |
| ]; | |
| const STYLE_DETAIL = { | |
| "No extra style":"", | |
| "Photorealistic":"photorealistic, realistic materials, physically accurate lighting, natural texture", | |
| "Ultra realistic photography":"ultra realistic professional photography, true-to-life optics, natural micro-texture", | |
| "Cinematic":"cinematic composition, dramatic motivated lighting, filmic depth, refined color grading", | |
| "Hollywood blockbuster":"large-scale blockbuster frame, premium production design, dynamic cinematic staging", | |
| "IMAX film still":"IMAX film still, monumental scale, crisp cinematic detail, natural film contrast", | |
| "35mm analog film":"35mm analog film, organic grain, subtle halation, authentic lens character", | |
| "Fashion editorial":"high-fashion editorial, deliberate styling, premium magazine art direction", | |
| "Luxury fashion campaign":"luxury fashion campaign, immaculate art direction, expensive materials", | |
| "Studio portrait":"professional studio portrait, controlled key and fill lighting, clean separation", | |
| "Product photography":"premium product photography, precise reflections, clean geometry, commercial detail", | |
| "Luxury product advertising":"luxury advertising campaign, sculpted highlights, polished surfaces, elegant negative space", | |
| "Film noir":"film noir, hard directional light, deep shadows, smoky suspenseful atmosphere", | |
| "Neo-noir":"neo-noir, modern darkness, colored practical lights, tense cinematic mood", | |
| "Volumetric lighting":"volumetric light shafts, atmospheric depth, visible light diffusion", | |
| "Cyberpunk":"cyberpunk city aesthetic, neon infrastructure, rain-slick surfaces, dense futuristic detail", | |
| "Solarpunk":"solarpunk, optimistic ecological technology, lush greenery, sustainable architecture", | |
| "Steampunk":"steampunk, intricate brass machinery, Victorian engineering, warm industrial atmosphere", | |
| "Sci-fi concept art":"high-end science-fiction concept art, believable technology, cinematic scale", | |
| "Space opera":"epic space opera, colossal celestial scale, ornate futuristic design", | |
| "Fantasy concept art":"premium fantasy concept art, rich worldbuilding, dramatic atmosphere", | |
| "Dark fantasy":"dark fantasy, ominous mythology, textured shadows, ancient atmosphere", | |
| "High fantasy":"high fantasy, majestic worldbuilding, luminous magic, heroic composition", | |
| "Oil painting":"museum-quality oil painting, layered brushwork, rich pigments, painterly light", | |
| "Watercolor":"expressive watercolor, translucent washes, granulation, soft edge variation", | |
| "Digital painting":"high-end digital painting, confident brushwork, cinematic rendering", | |
| "Anime":"premium anime illustration, expressive character design, clean linework, cinematic cel shading", | |
| "Manga":"detailed manga artwork, confident ink lines, dynamic composition", | |
| "90s anime":"1990s anime aesthetic, hand-painted cel look, nostalgic color palette", | |
| "Photorealistic 3D render":"photorealistic 3D render, physically based materials, ray-traced lighting", | |
| "Unreal Engine cinematic":"Unreal Engine cinematic frame, detailed assets, global illumination", | |
| "Pixel art":"carefully designed pixel art, readable silhouettes, deliberate limited palette", | |
| "PS1 game aesthetic":"PlayStation 1 aesthetic, low-poly geometry, retro affine texture character", | |
| "Japanese ukiyo-e":"Japanese ukiyo-e woodblock print, elegant flat shapes, carved line rhythm", | |
| "Islamic geometric art":"intricate Islamic geometric design, precise tessellation, balanced ornament", | |
| "Surrealism":"surrealist visual logic, uncanny symbolism, dreamlike spatial relationships", | |
| "Magical realism":"magical realism, believable everyday world touched by subtle wonder", | |
| "Art Nouveau":"Art Nouveau, flowing organic ornament, elegant curves, botanical detail", | |
| "Art Deco":"Art Deco, geometric luxury, symmetrical glamour, polished metallic accents", | |
| "Brutalism":"brutalist design, monumental concrete geometry, severe structural rhythm", | |
| "Minimalism":"minimalist composition, disciplined negative space, simple forms", | |
| "Maximalism":"maximalist visual abundance, layered ornament, rich color and detail", | |
| "Movie poster":"premium theatrical movie poster, iconic focal point, dramatic visual hierarchy", | |
| "Album cover":"memorable album cover artwork, bold identity, strong central concept", | |
| "Dark academia":"dark academia, old libraries, scholarly atmosphere, warm shadowed interiors", | |
| "Dreamcore":"dreamcore, nostalgic uncanny spaces, soft surreal atmosphere", | |
| "Vaporwave":"vaporwave, retro digital nostalgia, surreal neon geometry, glossy gradients", | |
| "Synthwave":"synthwave, retro-futurist neon, dramatic horizon glow, 1980s electronic atmosphere", | |
| "Elegant luxury":"elegant luxury aesthetic, refined materials, tasteful restraint, premium finish", | |
| "Clean commercial":"clean commercial image, polished lighting, clear subject, advertising-ready composition", | |
| "Hyper-detailed":"hyper-detailed, intricate micro-texture, crisp controlled complexity", | |
| "Ethereal":"ethereal atmosphere, luminous haze, delicate light, graceful visual softness", | |
| "Whimsical":"whimsical visual storytelling, imaginative forms, charming unexpected details", | |
| "Moody":"moody atmosphere, controlled contrast, emotionally charged lighting", | |
| "Haunting":"haunting beauty, restrained unease, atmospheric depth, memorable silhouette", | |
| "Cozy":"cozy atmosphere, warm practical light, tactile materials, inviting detail", | |
| "Epic":"epic scale, heroic composition, monumental depth, dramatic visual impact" | |
| }; | |
| const PROMPT_BOOST = | |
| "masterful composition, coherent anatomy, accurate hands and facial structure, " + | |
| "clear subject separation, refined color harmony, realistic material response, " + | |
| "controlled detail, intentional lighting, professional visual storytelling"; | |
| for (const style of STYLES) { | |
| const option = document.createElement("option"); | |
| option.value = style; | |
| $("styles").appendChild(option); | |
| } | |
| function normalise(value) { | |
| return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, ""); | |
| } | |
| function updateQueueUI() { | |
| $("queuedChip").textContent = `Queued: ${localQueue.length}`; | |
| $("activeChip").textContent = processing ? "Active: generating" : "Active: none"; | |
| } | |
| function setViewer(mode, message = "") { | |
| $("empty").style.display = mode === "empty" ? "block" : "none"; | |
| $("loading").style.display = mode === "loading" ? "block" : "none"; | |
| $("image").style.display = mode === "image" ? "block" : "none"; | |
| if (message) $("status").textContent = message; | |
| } | |
| function endpointInfo(names) { | |
| const named = api?.named_endpoints || {}; | |
| const entries = Object.entries(named); | |
| for (const name of names) { | |
| const exact = entries.find(([key]) => normalise(key) === normalise(name)); | |
| if (exact) return { endpoint: exact[0], spec: exact[1] }; | |
| } | |
| for (const name of names) { | |
| const partial = entries.find(([key]) => normalise(key).includes(normalise(name))); | |
| if (partial) return { endpoint: partial[0], spec: partial[1] }; | |
| } | |
| return { endpoint: names[0], spec: null }; | |
| } | |
| function buildPayload(spec, values, fallback) { | |
| const params = spec?.parameters; | |
| if (!Array.isArray(params) || !params.length) return fallback; | |
| return params.map(param => { | |
| const name = normalise(param.parameter_name || param.label || param.name || param.component); | |
| if (Object.prototype.hasOwnProperty.call(values, name)) return values[name]; | |
| for (const [alias, value] of Object.entries(values)) { | |
| if (alias && name && (name.includes(alias) || alias.includes(name))) return value; | |
| } | |
| if ("parameter_default" in param) return param.parameter_default; | |
| return null; | |
| }); | |
| } | |
| async function connectBackend() { | |
| if (client) return client; | |
| $("loadingTitle").textContent = "Connecting to the remote AIDMA engine…"; | |
| client = await Client.connect(BACKEND_SPACE, { | |
| events: ["data", "status"], | |
| space_status: status => { | |
| if (status?.status && status.status !== "running") { | |
| $("queueStatus").textContent = `Remote Space: ${status.status}`; | |
| } | |
| } | |
| }); | |
| api = await client.view_api(); | |
| return client; | |
| } | |
| function snapshotJob() { | |
| const prompt = $("prompt").value.trim(); | |
| if (!prompt) throw new Error("Write a prompt first."); | |
| const modelOption = $("model").selectedOptions[0]; | |
| const [width, height] = $("ratio").value.split("x").map(Number); | |
| const styleName = $("style").value.trim(); | |
| const styleText = STYLE_DETAIL[styleName] ?? styleName; | |
| const parts = [prompt]; | |
| if (styleText && normalise(styleName) !== normalise("No extra style")) parts.push(styleText); | |
| if ($("boost").checked) parts.push(PROMPT_BOOST); | |
| return { | |
| id: ++jobCounter, | |
| repo: modelOption.value, | |
| trigger: modelOption.dataset.trigger || "aidmaMJ6.1", | |
| modelLabel: modelOption.textContent.trim(), | |
| prompt: parts.join(", "), | |
| width, | |
| height, | |
| steps: Number($("quality").value), | |
| seed: crypto.getRandomValues(new Uint32Array(1))[0], | |
| cfg: 3.5, | |
| loraScale: 0.95 | |
| }; | |
| } | |
| function remoteFileUrl(value) { | |
| if (typeof value !== "string") return null; | |
| const text = value.trim(); | |
| if (!text) return null; | |
| // Already usable in the browser. | |
| if (/^(https?:\/\/|blob:|data:image\/)/i.test(text)) return text; | |
| // Modern Gradio may return a relative file route. | |
| if (text.startsWith("/gradio_api/")) return `${BACKEND_ORIGIN}${text}`; | |
| if (text.startsWith("gradio_api/")) return `${BACKEND_ORIGIN}/${text}`; | |
| // Some Gradio versions return only the server-side temporary path. | |
| if ( | |
| text.startsWith("/tmp/") || | |
| text.includes("/gradio/") || | |
| /\.(png|jpe?g|webp|gif|bmp)(\?|$)/i.test(text) | |
| ) { | |
| return `${BACKEND_ORIGIN}/gradio_api/file=${encodeURI(text)}`; | |
| } | |
| return null; | |
| } | |
| function extractImageUrl(value, seen = new Set()) { | |
| if (value == null) return null; | |
| if (typeof Blob !== "undefined" && value instanceof Blob) { | |
| return URL.createObjectURL(value); | |
| } | |
| if (typeof File !== "undefined" && value instanceof File) { | |
| return URL.createObjectURL(value); | |
| } | |
| if (typeof value === "string") { | |
| const trimmed = value.trim(); | |
| // Some component serializers return raw base64 rather than a FileData URL. | |
| if (/^[A-Za-z0-9+/=\s]{1000,}$/.test(trimmed)) { | |
| return `data:image/png;base64,${trimmed.replace(/\s+/g, "")}`; | |
| } | |
| return remoteFileUrl(trimmed); | |
| } | |
| if (typeof value !== "object") return null; | |
| if (seen.has(value)) return null; | |
| seen.add(value); | |
| if (Array.isArray(value)) { | |
| // The first output of /run_lora is the generated image. | |
| for (const item of value) { | |
| const found = extractImageUrl(item, seen); | |
| if (found) return found; | |
| } | |
| return null; | |
| } | |
| // Gradio FileData commonly uses url/path/orig_name/name. | |
| for (const key of ["url", "path", "orig_name", "name", "image", "data"]) { | |
| if (Object.prototype.hasOwnProperty.call(value, key)) { | |
| const found = extractImageUrl(value[key], seen); | |
| if (found) return found; | |
| } | |
| } | |
| for (const item of Object.values(value)) { | |
| const found = extractImageUrl(item, seen); | |
| if (found) return found; | |
| } | |
| return null; | |
| } | |
| function showImage(url, job) { | |
| $("image").src = url; | |
| $("download").href = url; | |
| $("download").download = `aidma-${job.seed}.png`; | |
| $("download").style.display = "inline-flex"; | |
| setViewer("image", `${job.modelLabel} · seed ${job.seed}`); | |
| } | |
| async function loadSelectedLora(job) { | |
| const { endpoint, spec } = endpointInfo(["/add_custom_lora", "add_custom_lora"]); | |
| const payload = buildPayload( | |
| spec, | |
| { | |
| customlora: job.repo, | |
| lorarepository: job.repo, | |
| huggingfacepath: job.repo | |
| }, | |
| [job.repo] | |
| ); | |
| const result = await client.predict(endpoint, payload); | |
| const data = result?.data || []; | |
| const selectedIndex = Number.isFinite(Number(data[4])) ? Number(data[4]) : null; | |
| const trigger = typeof data[5] === "string" && data[5].trim() ? data[5].trim() : job.trigger; | |
| return { selectedIndex, trigger }; | |
| } | |
| async function runGeneration(job, selectedIndex, trigger) { | |
| const { endpoint, spec } = endpointInfo(["/run_lora", "run_lora"]); | |
| const finalPrompt = normalise(job.prompt).includes(normalise(trigger)) | |
| ? job.prompt | |
| : `${trigger}, ${job.prompt}`; | |
| const values = { | |
| prompt: finalPrompt, | |
| prompttext: finalPrompt, | |
| imageinput: null, | |
| inputimage: null, | |
| image: null, | |
| imagestrength: 0.75, | |
| denoisestrength: 0.75, | |
| cfgscale: job.cfg, | |
| guidancescale: job.cfg, | |
| steps: job.steps, | |
| numinferencesteps: job.steps, | |
| selectedindex: selectedIndex, | |
| randomizeseed: false, | |
| seed: job.seed, | |
| width: job.width, | |
| height: job.height, | |
| lorascale: job.loraScale, | |
| adapterstrength: job.loraScale | |
| }; | |
| /* | |
| The explorer's generation function is a Python generator that emits preview | |
| frames. Using submit() here can leave a Static frontend holding only a | |
| progress event. predict() waits until the generator completes and returns | |
| the final three outputs: [image, seed, progress]. | |
| */ | |
| const fallbackWithoutState = [ | |
| finalPrompt, null, 0.75, job.cfg, job.steps, false, | |
| job.seed, job.width, job.height, job.loraScale | |
| ]; | |
| const payload = buildPayload(spec, values, fallbackWithoutState); | |
| $("queueStatus").textContent = "Waiting for the remote GPU and final image"; | |
| const response = await client.predict(endpoint, payload); | |
| // @gradio/client normally returns { type: "data", data: [...] }. | |
| // Keep compatibility with clients that return the output array directly. | |
| const outputs = response?.data ?? response; | |
| console.log("AIDMA final Gradio outputs:", outputs); | |
| // /run_lora outputs: [generated image, final seed, progress component]. | |
| const imageOutput = Array.isArray(outputs) ? outputs[0] : outputs; | |
| const returnedSeed = Array.isArray(outputs) ? Number(outputs[1]) : NaN; | |
| const finalUrl = extractImageUrl(imageOutput) || extractImageUrl(outputs); | |
| if (!finalUrl) { | |
| console.error("Unrecognized final Gradio image payload:", outputs); | |
| throw new Error( | |
| "The remote Space did not provide a usable final image. " + | |
| "It may have rejected this LoRA or changed its API." | |
| ); | |
| } | |
| if (Number.isFinite(returnedSeed)) job.seed = returnedSeed; | |
| showImage(finalUrl, job); | |
| return finalUrl; | |
| } | |
| async function processQueue() { | |
| if (processing || !localQueue.length) return; | |
| processing = true; | |
| updateQueueUI(); | |
| while (localQueue.length) { | |
| const job = localQueue.shift(); | |
| updateQueueUI(); | |
| $("download").style.display = "none"; | |
| $("loadingTitle").textContent = `Generating job #${job.id}…`; | |
| $("queueStatus").textContent = "Preparing AIDMA LoRA"; | |
| $("status").className = ""; | |
| setViewer("loading", `Job #${job.id} is running.`); | |
| try { | |
| await connectBackend(); | |
| const loaded = await loadSelectedLora(job); | |
| $("queueStatus").textContent = "Submitting to remote GPU and waiting for final output"; | |
| await runGeneration(job, loaded.selectedIndex, loaded.trigger); | |
| } catch (error) { | |
| console.error(error); | |
| $("status").className = "error"; | |
| $("status").textContent = error?.message || String(error); | |
| $("empty").innerHTML = | |
| '<div class="orb">!</div><strong>Generation failed.</strong>' + | |
| '<div style="margin-top:8px">' + escapeHtml(error?.message || String(error)) + '</div>'; | |
| setViewer("empty"); | |
| // Force a clean connection for the next queued job. | |
| client = null; | |
| api = null; | |
| } | |
| } | |
| processing = false; | |
| updateQueueUI(); | |
| } | |
| function escapeHtml(value) { | |
| return String(value).replace(/[&<>"']/g, char => ({ | |
| "&":"&","<":"<",">":">",'"':""","'":"'" | |
| })[char]); | |
| } | |
| $("generate").addEventListener("click", () => { | |
| try { | |
| const job = snapshotJob(); | |
| localQueue.push(job); | |
| $("status").className = ""; | |
| $("status").textContent = `Job #${job.id} added to your browser queue.`; | |
| updateQueueUI(); | |
| processQueue(); | |
| } catch (error) { | |
| $("status").className = "error"; | |
| $("status").textContent = error.message; | |
| $("prompt").focus(); | |
| } | |
| }); | |
| $("prompt").addEventListener("keydown", event => { | |
| if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { | |
| event.preventDefault(); | |
| $("generate").click(); | |
| } | |
| }); | |
| updateQueueUI(); | |
| </script> | |
| </body> | |
| </html> | |