hy3dlab commited on
Commit
4d9ddfa
·
verified ·
1 Parent(s): 8949bab

ui: restore general-mode Space editor

Browse files
Files changed (6) hide show
  1. README.md +9 -2
  2. app.py +22 -3
  3. assets/app-ui.js +869 -0
  4. assets/gradio-client-2.3.1.js +1971 -0
  5. index.html +714 -45
  6. tests/test_space_static.py +61 -6
README.md CHANGED
@@ -9,7 +9,7 @@ python_version: 3.10.13
9
  app_file: app.py
10
  pinned: true
11
  license: apache-2.0
12
- short_description: Revision-pinned general Image-to-Image API backend
13
  ---
14
 
15
  # hy3dlab Image-to-Image
@@ -22,6 +22,11 @@ Current mode: general image editing through the pinned replacement transformer.
22
  No LoRA adapter is loaded. The legacy `lora_adapter` API field is retained for
23
  compatibility and accepts only the reserved internal value `__base__`.
24
 
 
 
 
 
 
25
  ## API boundary
26
 
27
  - Named endpoint: `/edit_image`
@@ -49,7 +54,9 @@ Remote model, transformer, kernel, and candidate LoRA revisions are pinned in
49
  `provenance.py`. See `MODEL_BOM.md` for the recorded supply-chain facts and open
50
  release gates. Candidate LoRAs are provenance records only and none are enabled
51
  by the runtime. Python direct dependencies are pinned in `requirements.txt` and
52
- `pre-requirements.txt`; a Linux transitive lock remains a post-build task.
 
 
53
 
54
  No Hugging Face owner token is required by the browser-facing public API design.
55
  Deployment credentials must never be stored in this repository or sent to a
 
9
  app_file: app.py
10
  pinned: true
11
  license: apache-2.0
12
+ short_description: Revision-pinned general AI image editor and API
13
  ---
14
 
15
  # hy3dlab Image-to-Image
 
22
  No LoRA adapter is loaded. The legacy `lora_adapter` API field is retained for
23
  compatibility and accepts only the reserved internal value `__base__`.
24
 
25
+ The Space App includes a first-party single-image editor with upload, Prompt,
26
+ queue status, before/after comparison, session history, and PNG download. Its
27
+ browser code is self-hosted and uses the same strict seven-field API contract;
28
+ the removed upstream LoRA and example controls are not exposed.
29
+
30
  ## API boundary
31
 
32
  - Named endpoint: `/edit_image`
 
54
  `provenance.py`. See `MODEL_BOM.md` for the recorded supply-chain facts and open
55
  release gates. Candidate LoRAs are provenance records only and none are enabled
56
  by the runtime. Python direct dependencies are pinned in `requirements.txt` and
57
+ `pre-requirements.txt`. The browser client is vendored at the installed
58
+ `@gradio/client` 2.3.1 build and hash-checked by the static test suite; a Linux
59
+ transitive lock remains a post-build task.
60
 
61
  No Hugging Face owner token is required by the browser-facing public API design.
62
  Deployment credentials must never be stored in this repository or sent to a
app.py CHANGED
@@ -11,7 +11,8 @@ import threading
11
  import gradio as gr
12
  import spaces
13
  import torch
14
- from fastapi.responses import HTMLResponse
 
15
  from gradio import Server
16
 
17
  from i2i_contract import (
@@ -263,11 +264,29 @@ def client_config() -> dict:
263
  }
264
 
265
 
 
 
 
 
 
 
 
266
  @app.get("/", response_class=HTMLResponse)
267
- async def homepage() -> str:
268
  html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
269
  with open(html_path, "r", encoding="utf-8") as file:
270
- return file.read()
 
 
 
 
 
 
 
 
 
 
 
271
 
272
 
273
  if __name__ == "__main__":
 
11
  import gradio as gr
12
  import spaces
13
  import torch
14
+ from fastapi import HTTPException
15
+ from fastapi.responses import FileResponse, HTMLResponse
16
  from gradio import Server
17
 
18
  from i2i_contract import (
 
264
  }
265
 
266
 
267
+ NO_STORE_HEADERS = {
268
+ "Cache-Control": "no-store, max-age=0, must-revalidate",
269
+ "Pragma": "no-cache",
270
+ "Expires": "0",
271
+ }
272
+
273
+
274
  @app.get("/", response_class=HTMLResponse)
275
+ async def homepage() -> HTMLResponse:
276
  html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
277
  with open(html_path, "r", encoding="utf-8") as file:
278
+ return HTMLResponse(content=file.read(), headers=NO_STORE_HEADERS)
279
+
280
+
281
+ @app.get("/hy3d-assets/{asset_name}", include_in_schema=False)
282
+ async def ui_asset(asset_name: str) -> FileResponse:
283
+ """Serve the two allowlisted, revision-controlled browser assets."""
284
+
285
+ allowed_assets = {"app-ui.js", "gradio-client-2.3.1.js"}
286
+ if asset_name not in allowed_assets:
287
+ raise HTTPException(status_code=404, detail="Not found")
288
+ asset_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", asset_name)
289
+ return FileResponse(asset_path, media_type="application/javascript", headers=NO_STORE_HEADERS)
290
 
291
 
292
  if __name__ == "__main__":
assets/app-ui.js ADDED
@@ -0,0 +1,869 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Client } from "/hy3d-assets/gradio-client-2.3.1.js";
2
+
3
+ const GENERAL_MODE_ID = "__base__";
4
+ const MAX_HISTORY_ITEMS = 6;
5
+ const FALLBACK_LIMITS = Object.freeze({
6
+ input_images: 1,
7
+ mime_types: ["image/jpeg", "image/png", "image/webp"],
8
+ max_image_bytes: 10 * 1024 * 1024,
9
+ max_image_pixels: 16_000_000,
10
+ min_image_side: 64,
11
+ max_prompt_chars: 1000,
12
+ seed: [0, 2147483647],
13
+ guidance: [1, 10],
14
+ steps: [1, 50],
15
+ max_output_side: 1024,
16
+ });
17
+
18
+ const PUBLIC_ERROR_MESSAGES = Object.freeze({
19
+ I2I_BAD_REQUEST: "The request is invalid. Check the image and settings.",
20
+ I2I_UNSUPPORTED_TYPE: "Use a PNG, JPEG, or WebP image.",
21
+ I2I_FILE_TOO_LARGE: "The image is too large. Use a file up to 10 MB.",
22
+ I2I_IMAGE_DECODE_FAILED: "The image could not be read. Try another file.",
23
+ I2I_IMAGE_DIMENSIONS_INVALID: "The image dimensions are not supported.",
24
+ I2I_PROMPT_REQUIRED: "Enter an instruction for the edit.",
25
+ I2I_PROMPT_TOO_LONG: "The instruction is too long.",
26
+ I2I_LORA_NOT_ALLOWED: "The editor configuration is out of date. Refresh the page.",
27
+ I2I_PROVIDER_BUSY: "The editor is busy. Please try again shortly.",
28
+ I2I_INFERENCE_FAILED: "Image editing failed. Please try again.",
29
+ I2I_INVALID_OUTPUT: "The editor returned an invalid result. Please try again.",
30
+ });
31
+
32
+ const SUGGESTIONS = Object.freeze([
33
+ ["Studio Background", "Replace the background with a clean studio backdrop while preserving the subject."],
34
+ ["Warm Light", "Add warm natural lighting while keeping the subject unchanged."],
35
+ ["Cool Light", "Add cool balanced lighting while keeping the subject unchanged."],
36
+ ["B&W", "Convert the image to black and white with balanced contrast."],
37
+ ["Blur Background", "Add a natural shallow depth of field and softly blur the background."],
38
+ ["Sharpen", "Improve clarity and fine detail without changing the composition."],
39
+ ["Remove Object", "Remove the distracting object and reconstruct the background naturally."],
40
+ ["Product Photo", "Turn this into a clean professional product photo with even lighting."],
41
+ ]);
42
+
43
+ const galleryGrid = document.getElementById("image-gallery-grid");
44
+ const studio = document.querySelector(".studio");
45
+ const fileInput = document.getElementById("custom-file-input");
46
+ const btnUpload = document.getElementById("tb-upload");
47
+ const btnRemove = document.getElementById("tb-remove");
48
+ const btnClear = document.getElementById("tb-clear");
49
+ const promptInput = document.getElementById("custom-prompt-input");
50
+ const charCount = document.getElementById("char-count");
51
+ const runBtn = document.getElementById("custom-run-btn");
52
+ const runBtnLabel = document.getElementById("run-btn-label");
53
+ const cancelBtn = document.getElementById("custom-cancel-btn");
54
+ const imgCountTb = document.getElementById("tb-image-count");
55
+ const imgCountSb = document.getElementById("sb-image-count");
56
+ const sbStatus = document.getElementById("sb-status");
57
+ const loader = document.getElementById("output-loader");
58
+ const loaderText = document.getElementById("loader-text");
59
+ const loaderFill = document.getElementById("loader-bar-fill");
60
+ const outBody = document.getElementById("output-image-container");
61
+ const outPh = document.getElementById("output-placeholder");
62
+ const dlBtn = document.getElementById("dl-btn-output");
63
+ const compareBtn = document.getElementById("compare-btn");
64
+ const useAsInputBtn = document.getElementById("use-as-input-btn");
65
+ const zoomBtn = document.getElementById("zoom-btn");
66
+ const seedChip = document.getElementById("out-seed-chip");
67
+ const historyStrip = document.getElementById("history-strip");
68
+ const historyCount = document.getElementById("history-count");
69
+ const seedSlider = document.getElementById("custom-seed");
70
+ const seedVal = document.getElementById("custom-seed-val");
71
+ const diceBtn = document.getElementById("dice-btn");
72
+ const randomizeInput = document.getElementById("custom-randomize");
73
+ const guidanceSlider = document.getElementById("custom-guidance");
74
+ const stepsSlider = document.getElementById("custom-steps");
75
+ const lightbox = document.getElementById("lightbox");
76
+ const lightboxImg = document.getElementById("lightbox-img");
77
+ const lightboxClose = document.getElementById("lightbox-close");
78
+ const dropOverlay = document.getElementById("drop-overlay");
79
+ const compareWrap = document.getElementById("compare-wrap");
80
+ const compareTop = document.getElementById("compare-top");
81
+ const compareDivider = document.getElementById("compare-divider");
82
+ const compareBeforeImg = document.getElementById("compare-before-img");
83
+ const compareAfterImg = document.getElementById("compare-after-img");
84
+ const canvasMeta = document.getElementById("canvas-meta");
85
+ const segResult = document.getElementById("seg-result");
86
+ const segInput = document.getElementById("seg-input");
87
+ const connDot = document.getElementById("conn-dot");
88
+ const connText = document.getElementById("conn-text");
89
+ const suggestionsWrap = document.getElementById("suggestions-wrap");
90
+
91
+ let client = null;
92
+ let limits = FALLBACK_LIMITS;
93
+ let contractReady = false;
94
+ let inputImage = null;
95
+ let currentResult = null;
96
+ let history = [];
97
+ let historyIndex = -1;
98
+ let viewMode = "result";
99
+ let compareMode = false;
100
+ let compareFraction = .5;
101
+ let running = false;
102
+ let currentJob = null;
103
+ let currentStage = "idle";
104
+ let cancellationRequested = false;
105
+ let activeRunId = 0;
106
+ let selectionId = 0;
107
+ let toastTimer = null;
108
+
109
+ function errorClass(error) {
110
+ return error instanceof Error ? error.name : typeof error;
111
+ }
112
+
113
+ function extractPublicErrorCode(error) {
114
+ const text = typeof error === "string"
115
+ ? error
116
+ : error && typeof error.message === "string"
117
+ ? error.message
118
+ : "";
119
+ const match = text.match(/I2I_[A-Z_]+/);
120
+ return match && PUBLIC_ERROR_MESSAGES[match[0]] ? match[0] : "I2I_INFERENCE_FAILED";
121
+ }
122
+
123
+ function showToast(message, type = "error") {
124
+ let toast = document.getElementById("app-toast");
125
+ if (!toast) {
126
+ toast = document.createElement("div");
127
+ toast.id = "app-toast";
128
+ toast.className = "toast-notification";
129
+ const icon = document.createElement("span");
130
+ icon.className = "toast-icon";
131
+ const text = document.createElement("span");
132
+ text.className = "toast-text";
133
+ toast.append(icon, text);
134
+ document.body.appendChild(toast);
135
+ }
136
+ toast.className = `toast-notification ${type}`;
137
+ toast.setAttribute("role", type === "info" ? "status" : "alert");
138
+ toast.setAttribute("aria-live", type === "info" ? "polite" : "assertive");
139
+ toast.setAttribute("aria-atomic", "true");
140
+ toast.querySelector(".toast-icon").textContent = type === "warning" ? "⚠" : type === "info" ? "ℹ" : "✗";
141
+ toast.querySelector(".toast-text").textContent = message;
142
+ if (toastTimer) clearTimeout(toastTimer);
143
+ void toast.offsetWidth;
144
+ toast.classList.add("visible");
145
+ toastTimer = setTimeout(() => toast.classList.remove("visible"), 3500);
146
+ }
147
+
148
+ function setStatus(text, style = "") {
149
+ sbStatus.textContent = text;
150
+ sbStatus.className = `sb-pill${style ? ` ${style}` : ""}`;
151
+ }
152
+
153
+ function updateRunButton() {
154
+ runBtn.disabled = !contractReady || running || !inputImage;
155
+ }
156
+
157
+ function flashPromptError() {
158
+ promptInput.classList.add("error-flash");
159
+ promptInput.focus();
160
+ setTimeout(() => promptInput.classList.remove("error-flash"), 800);
161
+ }
162
+
163
+ function getCanvasImage() {
164
+ return outBody.querySelector("img.modern-out-img");
165
+ }
166
+
167
+ function exitCompareMode() {
168
+ if (!compareMode) return;
169
+ compareMode = false;
170
+ compareBtn.classList.remove("active");
171
+ compareBtn.setAttribute("aria-pressed", "false");
172
+ compareWrap.classList.remove("visible");
173
+ const canvasImage = getCanvasImage();
174
+ if (canvasImage && (currentResult || inputImage)) canvasImage.style.display = "";
175
+ }
176
+
177
+ function updateRailState() {
178
+ const hasResult = Boolean(currentResult);
179
+ btnUpload.disabled = running || !contractReady;
180
+ dlBtn.disabled = running || !hasResult;
181
+ useAsInputBtn.disabled = running || !hasResult;
182
+ compareBtn.disabled = running || !(hasResult && currentResult.beforeB64);
183
+ zoomBtn.disabled = running || !(hasResult || inputImage);
184
+ btnRemove.disabled = running || !inputImage;
185
+ btnClear.disabled = running || !inputImage;
186
+ }
187
+
188
+ function renderCanvas() {
189
+ exitCompareMode();
190
+ let canvasImage = getCanvasImage();
191
+ const shown = viewMode === "result" ? currentResult : inputImage;
192
+ if (shown) {
193
+ outPh.style.display = "none";
194
+ if (!canvasImage) {
195
+ canvasImage = document.createElement("img");
196
+ canvasImage.className = "modern-out-img";
197
+ canvasImage.alt = "Image preview";
198
+ canvasImage.addEventListener("click", () => {
199
+ if (!canvasImage.src) return;
200
+ openLightbox(canvasImage.src, canvasImage);
201
+ });
202
+ outBody.appendChild(canvasImage);
203
+ }
204
+ canvasImage.style.display = "";
205
+ canvasImage.src = viewMode === "result" ? currentResult.b64 : inputImage.b64;
206
+ canvasMeta.textContent = viewMode === "result"
207
+ ? `result · seed ${currentResult.seed}`
208
+ : `input · ${inputImage.name || "image"}`;
209
+ } else {
210
+ if (canvasImage) canvasImage.style.display = "none";
211
+ outPh.style.display = "";
212
+ canvasMeta.textContent = inputImage ? "Input ready — run an edit" : "No image loaded";
213
+ }
214
+ updateRailState();
215
+ }
216
+
217
+ function setViewMode(mode) {
218
+ viewMode = mode;
219
+ segResult.classList.toggle("active", mode === "result");
220
+ segInput.classList.toggle("active", mode === "input");
221
+ renderCanvas();
222
+ }
223
+
224
+ function updateCounts() {
225
+ const inputCount = inputImage ? 1 : 0;
226
+ imgCountTb.textContent = String(inputCount);
227
+ imgCountSb.textContent = inputCount
228
+ ? `1 input · ${history.length} result${history.length === 1 ? "" : "s"}`
229
+ : "No inputs";
230
+ }
231
+
232
+ function renderInputStrip() {
233
+ galleryGrid.replaceChildren();
234
+ if (!inputImage) {
235
+ const empty = document.createElement("div");
236
+ empty.className = "fs-empty";
237
+ empty.textContent = "Drop or paste one image…";
238
+ galleryGrid.appendChild(empty);
239
+ } else {
240
+ const thumb = document.createElement("div");
241
+ thumb.className = "fs-thumb selected";
242
+ const image = document.createElement("img");
243
+ image.src = inputImage.b64;
244
+ image.alt = "Input image";
245
+ const badge = document.createElement("span");
246
+ badge.className = "fs-badge";
247
+ badge.textContent = "#1";
248
+ const remove = document.createElement("button");
249
+ remove.className = "fs-remove";
250
+ remove.type = "button";
251
+ remove.textContent = "✕";
252
+ remove.setAttribute("aria-label", "Remove input image");
253
+ remove.addEventListener("click", (event) => {
254
+ event.stopPropagation();
255
+ clearInput();
256
+ });
257
+ thumb.append(image, badge, remove);
258
+ thumb.addEventListener("click", () => setViewMode("input"));
259
+ galleryGrid.appendChild(thumb);
260
+ }
261
+ const add = document.createElement("button");
262
+ add.type = "button";
263
+ add.className = "fs-add";
264
+ add.innerHTML = '<span class="add-icon">+</span><span class="add-text">Replace</span>';
265
+ add.addEventListener("click", () => fileInput.click());
266
+ galleryGrid.appendChild(add);
267
+ updateRailState();
268
+ }
269
+
270
+ function renderHistory() {
271
+ historyStrip.replaceChildren();
272
+ historyCount.textContent = String(history.length);
273
+ if (!history.length) {
274
+ const empty = document.createElement("div");
275
+ empty.className = "fs-empty";
276
+ empty.textContent = "No results yet";
277
+ historyStrip.appendChild(empty);
278
+ return;
279
+ }
280
+ history.forEach((entry, index) => {
281
+ const thumb = document.createElement("button");
282
+ thumb.type = "button";
283
+ thumb.className = `fs-thumb${index === historyIndex ? " selected" : ""}`;
284
+ thumb.title = `seed ${entry.seed}`;
285
+ const image = document.createElement("img");
286
+ image.src = entry.b64;
287
+ image.alt = `Edited result ${index + 1}`;
288
+ thumb.appendChild(image);
289
+ thumb.addEventListener("click", () => {
290
+ historyIndex = index;
291
+ currentResult = history[index];
292
+ seedChip.textContent = `seed ${currentResult.seed}`;
293
+ seedChip.classList.add("visible");
294
+ setViewMode("result");
295
+ renderHistory();
296
+ });
297
+ historyStrip.appendChild(thumb);
298
+ });
299
+ }
300
+
301
+ function clearInput() {
302
+ selectionId += 1;
303
+ inputImage = null;
304
+ fileInput.value = "";
305
+ renderInputStrip();
306
+ updateCounts();
307
+ renderCanvas();
308
+ updateRunButton();
309
+ }
310
+
311
+ function replaceInput(b64, name) {
312
+ inputImage = { b64, name };
313
+ renderInputStrip();
314
+ updateCounts();
315
+ setViewMode("input");
316
+ updateRunButton();
317
+ }
318
+
319
+ function readFileAsDataUrl(file) {
320
+ return new Promise((resolve, reject) => {
321
+ const reader = new FileReader();
322
+ reader.onload = () => typeof reader.result === "string" ? resolve(reader.result) : reject(new Error("invalid_reader_result"));
323
+ reader.onerror = () => reject(new Error("file_read_failed"));
324
+ reader.readAsDataURL(file);
325
+ });
326
+ }
327
+
328
+ function decodeImageFile(file) {
329
+ return new Promise((resolve, reject) => {
330
+ const url = URL.createObjectURL(file);
331
+ const image = new Image();
332
+ image.onload = () => {
333
+ URL.revokeObjectURL(url);
334
+ resolve({ width: image.naturalWidth, height: image.naturalHeight });
335
+ };
336
+ image.onerror = () => {
337
+ URL.revokeObjectURL(url);
338
+ reject(new Error("image_decode_failed"));
339
+ };
340
+ image.src = url;
341
+ });
342
+ }
343
+
344
+ async function processFile(file) {
345
+ if (!file) return;
346
+ if (!contractReady) {
347
+ showToast("The editor is still connecting. Try again shortly.", "warning");
348
+ return;
349
+ }
350
+ if (running) {
351
+ showToast("Wait for the current edit to finish.", "warning");
352
+ return;
353
+ }
354
+ const localSelectionId = ++selectionId;
355
+ if (!limits.mime_types.includes(file.type)) {
356
+ showToast(PUBLIC_ERROR_MESSAGES.I2I_UNSUPPORTED_TYPE);
357
+ return;
358
+ }
359
+ if (file.size <= 0 || file.size > limits.max_image_bytes) {
360
+ showToast(PUBLIC_ERROR_MESSAGES.I2I_FILE_TOO_LARGE);
361
+ return;
362
+ }
363
+ try {
364
+ const dimensions = await decodeImageFile(file);
365
+ if (
366
+ Math.min(dimensions.width, dimensions.height) < limits.min_image_side ||
367
+ dimensions.width * dimensions.height > limits.max_image_pixels
368
+ ) {
369
+ showToast(PUBLIC_ERROR_MESSAGES.I2I_IMAGE_DIMENSIONS_INVALID);
370
+ return;
371
+ }
372
+ const b64 = await readFileAsDataUrl(file);
373
+ if (localSelectionId !== selectionId) return;
374
+ replaceInput(b64, file.name);
375
+ showToast("Input image ready", "info");
376
+ } catch (error) {
377
+ console.warn("[image-editor] input validation failed", { error_class: errorClass(error) });
378
+ showToast(PUBLIC_ERROR_MESSAGES.I2I_IMAGE_DECODE_FAILED);
379
+ }
380
+ }
381
+
382
+ fileInput.addEventListener("change", () => {
383
+ const file = fileInput.files && fileInput.files[0];
384
+ void processFile(file);
385
+ fileInput.value = "";
386
+ });
387
+ btnUpload.addEventListener("click", () => fileInput.click());
388
+ btnRemove.addEventListener("click", clearInput);
389
+ btnClear.addEventListener("click", clearInput);
390
+
391
+ let dragDepth = 0;
392
+ window.addEventListener("dragenter", (event) => {
393
+ if (!event.dataTransfer || !Array.from(event.dataTransfer.types).includes("Files")) return;
394
+ event.preventDefault();
395
+ dragDepth += 1;
396
+ dropOverlay.classList.add("visible");
397
+ });
398
+ window.addEventListener("dragleave", (event) => {
399
+ if (!event.dataTransfer || !Array.from(event.dataTransfer.types).includes("Files")) return;
400
+ dragDepth = Math.max(0, dragDepth - 1);
401
+ if (!dragDepth) dropOverlay.classList.remove("visible");
402
+ });
403
+ window.addEventListener("dragover", (event) => event.preventDefault());
404
+ window.addEventListener("drop", (event) => {
405
+ event.preventDefault();
406
+ dragDepth = 0;
407
+ dropOverlay.classList.remove("visible");
408
+ const files = event.dataTransfer && event.dataTransfer.files;
409
+ if (!files || !files.length) return;
410
+ if (files.length > 1) showToast("Only the first image will be used.", "warning");
411
+ void processFile(files[0]);
412
+ });
413
+
414
+ document.addEventListener("paste", (event) => {
415
+ if (event.target === promptInput) return;
416
+ const items = event.clipboardData && event.clipboardData.items;
417
+ if (!items) return;
418
+ const item = Array.from(items).find((candidate) => candidate.kind === "file" && limits.mime_types.includes(candidate.type));
419
+ const file = item && item.getAsFile();
420
+ if (!file) return;
421
+ event.preventDefault();
422
+ void processFile(file);
423
+ });
424
+
425
+ SUGGESTIONS.forEach(([label, prompt]) => {
426
+ const chip = document.createElement("button");
427
+ chip.type = "button";
428
+ chip.className = "suggestion-chip";
429
+ chip.textContent = label;
430
+ chip.addEventListener("click", () => {
431
+ promptInput.value = prompt;
432
+ charCount.textContent = String(Array.from(prompt).length);
433
+ suggestionsWrap.querySelectorAll(".suggestion-chip").forEach((item) => item.classList.remove("active"));
434
+ chip.classList.add("active");
435
+ promptInput.focus();
436
+ updateRunButton();
437
+ });
438
+ suggestionsWrap.appendChild(chip);
439
+ });
440
+
441
+ promptInput.addEventListener("input", () => {
442
+ charCount.textContent = String(Array.from(promptInput.value).length);
443
+ suggestionsWrap.querySelectorAll(".suggestion-chip").forEach((item) => item.classList.remove("active"));
444
+ updateRunButton();
445
+ });
446
+
447
+ function bindSlider(slider) {
448
+ const value = document.getElementById(`${slider.id}-val`);
449
+ slider.addEventListener("input", () => { value.textContent = slider.value; });
450
+ }
451
+ bindSlider(seedSlider);
452
+ bindSlider(guidanceSlider);
453
+ bindSlider(stepsSlider);
454
+ diceBtn.addEventListener("click", () => {
455
+ const seed = Math.floor(Math.random() * (limits.seed[1] + 1));
456
+ seedSlider.value = String(seed);
457
+ seedVal.textContent = String(seed);
458
+ });
459
+ randomizeInput.addEventListener("change", () => {
460
+ seedSlider.disabled = randomizeInput.checked;
461
+ diceBtn.disabled = randomizeInput.checked;
462
+ seedSlider.parentElement.style.opacity = randomizeInput.checked ? ".55" : "1";
463
+ });
464
+ randomizeInput.dispatchEvent(new Event("change"));
465
+
466
+ function showLoader(text) {
467
+ loaderText.textContent = text;
468
+ loaderFill.classList.remove("determinate");
469
+ loaderFill.style.width = "100%";
470
+ loader.classList.add("active");
471
+ }
472
+
473
+ function setLoaderProgress(text, fraction) {
474
+ loaderText.textContent = text;
475
+ if (Number.isFinite(fraction) && fraction >= 0 && fraction <= 1) {
476
+ loaderFill.classList.add("determinate");
477
+ loaderFill.style.width = `${Math.max(4, Math.round(fraction * 100))}%`;
478
+ } else {
479
+ loaderFill.classList.remove("determinate");
480
+ loaderFill.style.width = "100%";
481
+ }
482
+ }
483
+
484
+ function hideLoader(statusText, style = "") {
485
+ loader.classList.remove("active");
486
+ setStatus(statusText, style);
487
+ }
488
+
489
+ function validateResult(output) {
490
+ if (
491
+ !output ||
492
+ typeof output.image !== "string" ||
493
+ !/^data:image\/png;base64,[A-Za-z0-9+/]+={0,2}$/.test(output.image) ||
494
+ !Number.isSafeInteger(output.seed) ||
495
+ output.seed < limits.seed[0] ||
496
+ output.seed > limits.seed[1]
497
+ ) {
498
+ throw new Error("I2I_INVALID_OUTPUT");
499
+ }
500
+ return { image: output.image, seed: output.seed };
501
+ }
502
+
503
+ function decodeOutput(dataUrl) {
504
+ return new Promise((resolve, reject) => {
505
+ const image = new Image();
506
+ image.onload = () => {
507
+ if (!image.naturalWidth || !image.naturalHeight || Math.max(image.naturalWidth, image.naturalHeight) > limits.max_output_side) {
508
+ reject(new Error("I2I_INVALID_OUTPUT"));
509
+ return;
510
+ }
511
+ resolve();
512
+ };
513
+ image.onerror = () => reject(new Error("I2I_INVALID_OUTPUT"));
514
+ image.src = dataUrl;
515
+ });
516
+ }
517
+
518
+ function showResult(entry) {
519
+ currentResult = entry;
520
+ seedChip.textContent = `seed ${entry.seed}`;
521
+ seedChip.classList.add("visible");
522
+ setViewMode("result");
523
+ }
524
+
525
+ function pushHistory(entry) {
526
+ history.push(entry);
527
+ if (history.length > MAX_HISTORY_ITEMS) history.shift();
528
+ historyIndex = history.length - 1;
529
+ renderHistory();
530
+ updateCounts();
531
+ }
532
+
533
+ function setComparePosition(fraction) {
534
+ const bounded = Math.max(0, Math.min(1, fraction));
535
+ compareFraction = bounded;
536
+ const width = compareWrap.clientWidth;
537
+ const x = bounded * width;
538
+ compareTop.style.clipPath = `inset(0 ${width - x}px 0 0)`;
539
+ compareDivider.style.left = `${x}px`;
540
+ compareWrap.setAttribute("aria-valuenow", String(Math.round(bounded * 100)));
541
+ }
542
+
543
+ function enterCompareMode() {
544
+ if (!currentResult || !currentResult.beforeB64) return;
545
+ compareMode = true;
546
+ compareBtn.classList.add("active");
547
+ compareBtn.setAttribute("aria-pressed", "true");
548
+ const canvasImage = getCanvasImage();
549
+ if (canvasImage) canvasImage.style.display = "none";
550
+ compareAfterImg.src = currentResult.b64;
551
+ compareBeforeImg.src = currentResult.beforeB64;
552
+ compareWrap.classList.add("visible");
553
+ requestAnimationFrame(() => setComparePosition(compareFraction));
554
+ }
555
+
556
+ compareBtn.addEventListener("click", () => compareMode ? exitCompareMode() : enterCompareMode());
557
+ let compareDragging = false;
558
+ compareWrap.addEventListener("pointerdown", (event) => {
559
+ compareDragging = true;
560
+ compareWrap.setPointerCapture(event.pointerId);
561
+ const rect = compareWrap.getBoundingClientRect();
562
+ setComparePosition((event.clientX - rect.left) / rect.width);
563
+ });
564
+ compareWrap.addEventListener("pointermove", (event) => {
565
+ if (!compareDragging) return;
566
+ const rect = compareWrap.getBoundingClientRect();
567
+ setComparePosition((event.clientX - rect.left) / rect.width);
568
+ });
569
+ compareWrap.addEventListener("pointerup", () => { compareDragging = false; });
570
+ compareWrap.addEventListener("pointercancel", () => { compareDragging = false; });
571
+ compareWrap.addEventListener("keydown", (event) => {
572
+ if (!compareMode) return;
573
+ let next = compareFraction;
574
+ if (event.key === "ArrowLeft" || event.key === "ArrowDown") next -= .05;
575
+ else if (event.key === "ArrowRight" || event.key === "ArrowUp") next += .05;
576
+ else if (event.key === "Home") next = 0;
577
+ else if (event.key === "End") next = 1;
578
+ else return;
579
+ event.preventDefault();
580
+ setComparePosition(next);
581
+ });
582
+
583
+ segResult.addEventListener("click", () => setViewMode("result"));
584
+ segInput.addEventListener("click", () => setViewMode("input"));
585
+ let lightboxReturnFocus = null;
586
+ function openLightbox(source, returnFocus) {
587
+ if (!source) return;
588
+ lightboxReturnFocus = returnFocus || document.activeElement;
589
+ lightboxImg.src = source;
590
+ lightbox.classList.add("visible");
591
+ lightbox.setAttribute("aria-hidden", "false");
592
+ studio.inert = true;
593
+ lightboxClose.focus();
594
+ }
595
+ function closeLightbox() {
596
+ if (!lightbox.classList.contains("visible")) return;
597
+ lightbox.classList.remove("visible");
598
+ lightbox.setAttribute("aria-hidden", "true");
599
+ lightboxImg.removeAttribute("src");
600
+ studio.inert = false;
601
+ if (lightboxReturnFocus && typeof lightboxReturnFocus.focus === "function") lightboxReturnFocus.focus();
602
+ lightboxReturnFocus = null;
603
+ }
604
+ lightbox.addEventListener("click", (event) => {
605
+ if (event.target === lightbox) closeLightbox();
606
+ });
607
+ lightboxClose.addEventListener("click", closeLightbox);
608
+ document.addEventListener("keydown", (event) => {
609
+ if (event.key === "Escape") closeLightbox();
610
+ });
611
+ zoomBtn.addEventListener("click", () => {
612
+ const source = viewMode === "result" && currentResult ? currentResult.b64 : inputImage && inputImage.b64;
613
+ if (!source) return;
614
+ openLightbox(source, zoomBtn);
615
+ });
616
+ dlBtn.addEventListener("click", () => {
617
+ if (!currentResult) return;
618
+ const link = document.createElement("a");
619
+ link.href = currentResult.b64;
620
+ link.download = `hy3d-image-edit-${currentResult.seed}.png`;
621
+ document.body.appendChild(link);
622
+ link.click();
623
+ link.remove();
624
+ });
625
+ useAsInputBtn.addEventListener("click", () => {
626
+ if (!currentResult) return;
627
+ replaceInput(currentResult.b64, `hy3d-image-edit-${currentResult.seed}.png`);
628
+ showToast("Result is ready for another edit", "info");
629
+ });
630
+ seedChip.addEventListener("click", () => {
631
+ if (!currentResult || !navigator.clipboard) return;
632
+ navigator.clipboard.writeText(String(currentResult.seed)).then(
633
+ () => showToast(`Seed copied: ${currentResult.seed}`, "info"),
634
+ () => undefined,
635
+ );
636
+ });
637
+
638
+ function setRunningUi(isRunning, stage = "idle") {
639
+ running = isRunning;
640
+ currentStage = stage;
641
+ runBtnLabel.textContent = isRunning ? "Editing…" : "Edit Image";
642
+ cancelBtn.style.display = isRunning && stage === "pending" ? "flex" : "none";
643
+ updateRailState();
644
+ updateRunButton();
645
+ }
646
+
647
+ function validateBeforeRun() {
648
+ if (!contractReady || !client) {
649
+ showToast("The editor is not connected. Refresh the page.");
650
+ return false;
651
+ }
652
+ if (!inputImage) {
653
+ showToast("Upload one image first.");
654
+ return false;
655
+ }
656
+ const prompt = promptInput.value.trim();
657
+ if (!prompt) {
658
+ showToast(PUBLIC_ERROR_MESSAGES.I2I_PROMPT_REQUIRED, "warning");
659
+ flashPromptError();
660
+ return false;
661
+ }
662
+ if (Array.from(prompt).length > limits.max_prompt_chars) {
663
+ showToast(PUBLIC_ERROR_MESSAGES.I2I_PROMPT_TOO_LONG);
664
+ return false;
665
+ }
666
+ return true;
667
+ }
668
+
669
+ async function runEdit() {
670
+ if (running || !validateBeforeRun()) return;
671
+ const runId = ++activeRunId;
672
+ cancellationRequested = false;
673
+ const beforeB64 = inputImage.b64;
674
+ const prompt = promptInput.value.trim();
675
+ let parsedResult = null;
676
+ setRunningUi(true, "preparing");
677
+ setViewMode("result");
678
+ showLoader("Submitting to queue…");
679
+ setStatus("Queued");
680
+
681
+ const payload = {
682
+ images_b64_json: JSON.stringify([beforeB64]),
683
+ prompt,
684
+ lora_adapter: GENERAL_MODE_ID,
685
+ seed: Number.parseInt(seedSlider.value, 10),
686
+ randomize_seed: randomizeInput.checked,
687
+ guidance_scale: Number.parseFloat(guidanceSlider.value),
688
+ steps: Number.parseInt(stepsSlider.value, 10),
689
+ };
690
+
691
+ try {
692
+ currentJob = client.submit("/edit_image", payload);
693
+ const eventId = await currentJob.wait_for_id();
694
+ if (runId !== activeRunId) return;
695
+ if (typeof eventId !== "string" || !eventId) throw new Error("I2I_PROVIDER_BUSY");
696
+ setRunningUi(true, "pending");
697
+ for await (const message of currentJob) {
698
+ if (runId !== activeRunId) return;
699
+ if (!message || typeof message.type !== "string") throw new Error("I2I_INVALID_OUTPUT");
700
+ if (message.type === "status") {
701
+ const stage = message.stage || message.status;
702
+ if (stage === "pending") {
703
+ setRunningUi(true, "pending");
704
+ const position = Number.isInteger(message.position) ? message.position + 1 : null;
705
+ const size = Number.isInteger(message.size) ? message.size : message.queue_size;
706
+ const label = position ? `In queue — position ${position}${size ? ` of ${size}` : ""}` : "Waiting in queue…";
707
+ setLoaderProgress(label, null);
708
+ setStatus("Queued");
709
+ } else if (stage === "generating" || stage === "streaming") {
710
+ setRunningUi(true, "generating");
711
+ setStatus("Processing…");
712
+ const progress = Array.isArray(message.progress_data) && message.progress_data.length
713
+ ? message.progress_data[message.progress_data.length - 1]
714
+ : null;
715
+ const fraction = progress && Number.isFinite(progress.index) && Number.isFinite(progress.length) && progress.length > 0
716
+ ? progress.index / progress.length
717
+ : null;
718
+ setLoaderProgress(fraction === null ? "Generating image…" : `Generating… ${Math.round(fraction * 100)}%`, fraction);
719
+ } else if (stage === "error") {
720
+ throw new Error(extractPublicErrorCode(message));
721
+ }
722
+ } else if (message.type === "data") {
723
+ if (parsedResult) throw new Error("I2I_INVALID_OUTPUT");
724
+ const output = validateResult(message.data && message.data[0]);
725
+ await decodeOutput(output.image);
726
+ parsedResult = {
727
+ b64: output.image,
728
+ seed: output.seed,
729
+ prompt,
730
+ beforeB64,
731
+ };
732
+ }
733
+ }
734
+ if (cancellationRequested) {
735
+ hideLoader("Stopped waiting");
736
+ showToast("Stopped waiting for this job. The backend may still finish it.", "warning");
737
+ return;
738
+ }
739
+ if (!parsedResult) throw new Error("I2I_INVALID_OUTPUT");
740
+ showResult(parsedResult);
741
+ pushHistory(parsedResult);
742
+ seedSlider.value = String(parsedResult.seed);
743
+ seedVal.textContent = String(parsedResult.seed);
744
+ hideLoader("Done", "done");
745
+ showToast("Image edited successfully", "info");
746
+ } catch (error) {
747
+ if (cancellationRequested) {
748
+ hideLoader("Stopped waiting");
749
+ showToast("Stopped waiting for this job. The backend may still finish it.", "warning");
750
+ } else {
751
+ const code = extractPublicErrorCode(error);
752
+ console.warn("[image-editor] request failed", { error_code: code, error_class: errorClass(error) });
753
+ hideLoader("Error", "error");
754
+ showToast(PUBLIC_ERROR_MESSAGES[code]);
755
+ }
756
+ } finally {
757
+ if (runId === activeRunId) {
758
+ currentJob = null;
759
+ cancellationRequested = false;
760
+ setRunningUi(false);
761
+ }
762
+ }
763
+ }
764
+
765
+ runBtn.addEventListener("click", () => void runEdit());
766
+ cancelBtn.addEventListener("click", async () => {
767
+ if (!currentJob || currentStage !== "pending") return;
768
+ const job = currentJob;
769
+ cancellationRequested = true;
770
+ cancelBtn.style.display = "none";
771
+ setLoaderProgress("Cancellation requested…", null);
772
+ setStatus("Cancelling…");
773
+ try {
774
+ await job.cancel();
775
+ } catch (error) {
776
+ console.warn("[image-editor] queue cancellation failed", { error_class: errorClass(error) });
777
+ } finally {
778
+ await job.return();
779
+ }
780
+ });
781
+ document.addEventListener("keydown", (event) => {
782
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
783
+ event.preventDefault();
784
+ void runEdit();
785
+ }
786
+ });
787
+
788
+ function validateConfig(config) {
789
+ if (
790
+ !config ||
791
+ config.edit_mode !== "general" ||
792
+ config.uses_lora !== false ||
793
+ config.required_lora_adapter_value !== GENERAL_MODE_ID ||
794
+ config.content_moderation_enabled !== false ||
795
+ !Array.isArray(config.loras) ||
796
+ config.loras.length !== 0 ||
797
+ !config.limits
798
+ ) {
799
+ throw new Error("contract_mismatch");
800
+ }
801
+ const nextLimits = config.limits;
802
+ const expectedMimes = ["image/jpeg", "image/png", "image/webp"];
803
+ if (
804
+ !Array.isArray(nextLimits.mime_types) ||
805
+ nextLimits.mime_types.length !== expectedMimes.length ||
806
+ !expectedMimes.every((mime) => nextLimits.mime_types.includes(mime)) ||
807
+ nextLimits.input_images !== 1 ||
808
+ !Number.isInteger(nextLimits.max_image_bytes) || nextLimits.max_image_bytes <= 0 ||
809
+ !Number.isInteger(nextLimits.max_image_pixels) || nextLimits.max_image_pixels <= 0 ||
810
+ !Number.isInteger(nextLimits.min_image_side) || nextLimits.min_image_side <= 0 ||
811
+ !Number.isInteger(nextLimits.max_prompt_chars) || nextLimits.max_prompt_chars <= 0 ||
812
+ !Array.isArray(nextLimits.seed) ||
813
+ !Array.isArray(nextLimits.guidance) ||
814
+ !Array.isArray(nextLimits.steps) ||
815
+ nextLimits.seed.length !== 2 || !nextLimits.seed.every(Number.isInteger) || nextLimits.seed[0] < 0 || nextLimits.seed[0] > nextLimits.seed[1] ||
816
+ nextLimits.guidance.length !== 2 || !nextLimits.guidance.every(Number.isFinite) || nextLimits.guidance[0] > nextLimits.guidance[1] ||
817
+ nextLimits.steps.length !== 2 || !nextLimits.steps.every(Number.isInteger) || nextLimits.steps[0] < 1 || nextLimits.steps[0] > nextLimits.steps[1] ||
818
+ !Number.isInteger(nextLimits.max_output_side) || nextLimits.max_output_side <= 0
819
+ ) {
820
+ throw new Error("invalid_limits");
821
+ }
822
+ return nextLimits;
823
+ }
824
+
825
+ function applyLimits(nextLimits) {
826
+ limits = nextLimits;
827
+ promptInput.maxLength = limits.max_prompt_chars;
828
+ seedSlider.min = String(limits.seed[0]);
829
+ seedSlider.max = String(limits.seed[1]);
830
+ guidanceSlider.min = String(limits.guidance[0]);
831
+ guidanceSlider.max = String(limits.guidance[1]);
832
+ stepsSlider.min = String(limits.steps[0]);
833
+ stepsSlider.max = String(limits.steps[1]);
834
+ }
835
+
836
+ async function initialize() {
837
+ runBtn.disabled = true;
838
+ try {
839
+ const [configResponse, connectedClient] = await Promise.all([
840
+ fetch("/api/config", { cache: "no-store" }),
841
+ Client.connect(window.location.origin, { events: ["data", "status"] }),
842
+ ]);
843
+ if (!configResponse.ok) throw new Error("config_unavailable");
844
+ applyLimits(validateConfig(await configResponse.json()));
845
+ client = connectedClient;
846
+ contractReady = true;
847
+ connDot.classList.remove("off", "pending");
848
+ connText.textContent = "connected";
849
+ setStatus("Ready");
850
+ } catch (error) {
851
+ console.warn("[image-editor] initialization failed", { error_class: errorClass(error) });
852
+ contractReady = false;
853
+ connDot.classList.remove("pending");
854
+ connDot.classList.add("off");
855
+ connText.textContent = "offline";
856
+ setStatus("Offline", "error");
857
+ showToast("Could not connect to the image editor. Refresh the page.");
858
+ } finally {
859
+ updateRailState();
860
+ updateRunButton();
861
+ }
862
+ }
863
+
864
+ renderInputStrip();
865
+ renderHistory();
866
+ updateCounts();
867
+ renderCanvas();
868
+ updateRunButton();
869
+ void initialize();
assets/gradio-client-2.3.1.js ADDED
@@ -0,0 +1,1971 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const Ke = "host", xe = "queue/data", Ve = "queue/join", ye = "upload", Ye = "login", K = "config", Qe = "info", Xe = "runtime", et = "sleeptime", tt = "heartbeat", st = "component_server", nt = "reset", it = "cancel", ot = "app_id", De = "This application is currently busy. Please try again. ", N = "Connection errored out. ", L = "Could not resolve app config. ", rt = "Could not get space status. ", at = "Could not get API info. ", ue = "Space metadata could not be loaded. ", ct = "Invalid URL. A full URL path is required.", ut = "Not authorized to access this space. ", Le = "Invalid credentials. Could not login. ", lt = "Login credentials are required to access this space.", pt = "File system access is only available in Node.js environments", Ne = "Root URL not found in client config", dt = "Error uploading file";
2
+ async function be(e, s, t) {
3
+ try {
4
+ return (await (await fetch(`https://huggingface.co/api/spaces/${e}/jwt`, {
5
+ headers: {
6
+ Authorization: `Bearer ${s}`,
7
+ ...t ? { Cookie: t } : {}
8
+ }
9
+ })).json()).token || !1;
10
+ } catch {
11
+ return !1;
12
+ }
13
+ }
14
+ function ht(e) {
15
+ let s = {};
16
+ return e.forEach(({ api_name: t, id: n }) => {
17
+ t && (s[t] = n);
18
+ }), s;
19
+ }
20
+ async function ft(e) {
21
+ const s = this.options.token ? { Authorization: `Bearer ${this.options.token}` } : {};
22
+ if (typeof window < "u" && window.gradio_config && location.origin !== "http://localhost:9876") {
23
+ if (window.gradio_config.current_page && (e = e.substring(0, e.lastIndexOf("/"))), window.gradio_config.dev_mode || typeof window < "u" && window?.BUILD_MODE === "dev") {
24
+ let t = ae(
25
+ e,
26
+ this.deep_link ? K + "?deep_link=" + this.deep_link : K
27
+ );
28
+ const n = await this.fetch(t, {
29
+ headers: s,
30
+ credentials: this.options.credentials ?? "same-origin"
31
+ }), o = await ve(n, !!this.options.auth);
32
+ o.root = e || o.root, window.gradio_config = {
33
+ ...o,
34
+ current_page: window.gradio_config.current_page
35
+ };
36
+ }
37
+ return { ...window.gradio_config };
38
+ } else if (e) {
39
+ let t = ae(
40
+ e,
41
+ this.deep_link ? K + "?deep_link=" + this.deep_link : K
42
+ );
43
+ const n = await this.fetch(t, {
44
+ headers: s,
45
+ credentials: this.options.credentials ?? "same-origin"
46
+ }), o = await ve(n, !!this.options.auth);
47
+ return o.root || (o.root = e), o;
48
+ }
49
+ throw new Error(L);
50
+ }
51
+ async function ve(e, s) {
52
+ if (e?.status === 401 && !s) {
53
+ const n = (await e.json())?.detail?.auth_message;
54
+ throw new Error(n || lt);
55
+ } else if (e?.status === 401 && s)
56
+ throw new Error(Le);
57
+ if (e?.status === 200) {
58
+ let t = await e.json();
59
+ return t.dependencies?.forEach((n, o) => {
60
+ n.id === void 0 && (n.id = o);
61
+ }), t;
62
+ } else if (e?.status === 401)
63
+ throw new Error(ut);
64
+ throw new Error(L);
65
+ }
66
+ async function _t() {
67
+ const { http_protocol: e, host: s } = await pe(
68
+ this.app_reference,
69
+ this.options.token
70
+ );
71
+ try {
72
+ if (this.options.auth) {
73
+ const t = await Ce(
74
+ e,
75
+ s,
76
+ this.options.auth,
77
+ this.fetch,
78
+ this.options.token,
79
+ this.options.credentials
80
+ );
81
+ t && this.set_cookies(t);
82
+ }
83
+ } catch (t) {
84
+ throw Error(t.message);
85
+ }
86
+ }
87
+ async function Ce(e, s, t, n, o, r) {
88
+ const i = new FormData();
89
+ i.append("username", t?.[0]), i.append("password", t?.[1]);
90
+ let a = {};
91
+ o && (a.Authorization = `Bearer ${o}`);
92
+ const u = await n(`${e}//${s}/${Ye}`, {
93
+ headers: a,
94
+ method: "POST",
95
+ body: i,
96
+ credentials: r ?? "same-origin"
97
+ });
98
+ if (u.status === 200)
99
+ return u.headers.get("set-cookie");
100
+ throw u.status === 401 ? new Error(Le) : new Error(ue);
101
+ }
102
+ function re(e) {
103
+ if (e.startsWith("http")) {
104
+ const { protocol: s, host: t, pathname: n } = new URL(e);
105
+ return {
106
+ ws_protocol: s === "https:" ? "wss" : "ws",
107
+ http_protocol: s,
108
+ host: t + (n !== "/" ? n : "")
109
+ };
110
+ }
111
+ return {
112
+ ws_protocol: "wss",
113
+ http_protocol: "https:",
114
+ host: new URL(e).host
115
+ };
116
+ }
117
+ const Pe = (e) => {
118
+ let s = [];
119
+ return e.split(/,(?=\s*[^\s=;]+=[^\s=;]+)/).forEach((n) => {
120
+ const [o, r] = n.split(";")[0].split("=");
121
+ o && r && s.push(`${o.trim()}=${r.trim()}`);
122
+ }), s;
123
+ }, le = /^[a-zA-Z0-9_\-\.]+\/[a-zA-Z0-9_\-\.]+$/, gt = /.*hf\.space\/{0,1}.*$/;
124
+ async function pe(e, s) {
125
+ const t = {};
126
+ s && (t.Authorization = `Bearer ${s}`);
127
+ const n = e.trim().replace(/\/$/, "");
128
+ if (le.test(n))
129
+ try {
130
+ const r = (await (await fetch(
131
+ `https://huggingface.co/api/spaces/${n}/${Ke}`,
132
+ { headers: t }
133
+ )).json()).host;
134
+ return {
135
+ space_id: e,
136
+ ...re(r)
137
+ };
138
+ } catch {
139
+ throw new Error(ue);
140
+ }
141
+ if (gt.test(n)) {
142
+ const { ws_protocol: o, http_protocol: r, host: i } = re(n);
143
+ return {
144
+ space_id: i.split("/")[0].replace(".hf.space", ""),
145
+ ws_protocol: o,
146
+ http_protocol: r,
147
+ host: i
148
+ };
149
+ }
150
+ return {
151
+ space_id: !1,
152
+ ...re(n)
153
+ };
154
+ }
155
+ const ae = (...e) => {
156
+ try {
157
+ return e.reduce((s, t) => (s = s.replace(/\/+$/, ""), t = t.replace(/^\/+/, ""), new URL(t, s + "/").toString()));
158
+ } catch {
159
+ throw new Error(ct);
160
+ }
161
+ };
162
+ function mt(e, s, t) {
163
+ const n = {
164
+ named_endpoints: {},
165
+ unnamed_endpoints: {}
166
+ };
167
+ return Object.keys(e).forEach((o) => {
168
+ (o === "named_endpoints" || o === "unnamed_endpoints") && (n[o] = {}, Object.entries(e[o]).forEach(
169
+ ([r, { parameters: i, returns: a }]) => {
170
+ const u = s.dependencies.find(
171
+ (c) => c.api_name === r || c.api_name === r.replace("/", "")
172
+ )?.id || t[r.replace("/", "")] || -1, l = u !== -1 ? s.dependencies.find((c) => c.id == u)?.types : { generator: !1, cancel: !1 };
173
+ if (u !== -1 && s.dependencies.find((c) => c.id == u)?.inputs?.length !== i.length) {
174
+ const c = s.dependencies.find((g) => g.id == u).inputs.map(
175
+ (g) => s.components.find((w) => w.id === g)?.type
176
+ );
177
+ try {
178
+ c.forEach((g, w) => {
179
+ if (g === "state") {
180
+ const C = {
181
+ component: "state",
182
+ example: null,
183
+ parameter_default: null,
184
+ parameter_has_default: !0,
185
+ parameter_name: null,
186
+ hidden: !0
187
+ };
188
+ i.splice(w, 0, C);
189
+ }
190
+ });
191
+ } catch (g) {
192
+ console.error(g);
193
+ }
194
+ }
195
+ const p = (c, g, w, C) => ({
196
+ ...c,
197
+ description: yt(c?.type, w),
198
+ type: wt(c?.type, g, w, C) || ""
199
+ });
200
+ n[o][r] = {
201
+ parameters: i.map(
202
+ (c) => p(c, c?.component, c?.serializer, "parameter")
203
+ ),
204
+ returns: a.map(
205
+ (c) => p(c, c?.component, c?.serializer, "return")
206
+ ),
207
+ type: l
208
+ };
209
+ }
210
+ ));
211
+ }), n;
212
+ }
213
+ function wt(e, s, t, n) {
214
+ if (s === "Api") return e.type;
215
+ switch (e?.type) {
216
+ case "string":
217
+ return "string";
218
+ case "boolean":
219
+ return "boolean";
220
+ case "number":
221
+ return "number";
222
+ }
223
+ if (t === "JSONSerializable" || t === "StringSerializable")
224
+ return "any";
225
+ if (t === "ListStringSerializable")
226
+ return "string[]";
227
+ if (s === "Image")
228
+ return n === "parameter" ? "Blob | File | Buffer" : "string";
229
+ if (t === "FileSerializable")
230
+ return e?.type === "array" ? n === "parameter" ? "(Blob | File | Buffer)[]" : "{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}[]" : n === "parameter" ? "Blob | File | Buffer" : "{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}";
231
+ if (t === "GallerySerializable")
232
+ return n === "parameter" ? "[(Blob | File | Buffer), (string | null)][]" : "[{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}, (string | null))][]";
233
+ }
234
+ function yt(e, s) {
235
+ return s === "GallerySerializable" ? "array of [file, label] tuples" : s === "ListStringSerializable" ? "array of strings" : s === "FileSerializable" ? "array of files or single file" : e?.description;
236
+ }
237
+ function Ee(e, s) {
238
+ switch (e.msg) {
239
+ case "send_data":
240
+ return { type: "data" };
241
+ case "send_hash":
242
+ return { type: "hash" };
243
+ case "queue_full":
244
+ return {
245
+ type: "update",
246
+ status: {
247
+ queue: !0,
248
+ message: De,
249
+ stage: "error",
250
+ code: e.code,
251
+ success: e.success
252
+ }
253
+ };
254
+ case "heartbeat":
255
+ return {
256
+ type: "heartbeat"
257
+ };
258
+ case "unexpected_error":
259
+ return {
260
+ type: "unexpected_error",
261
+ status: {
262
+ queue: !0,
263
+ message: e.message,
264
+ session_not_found: e.session_not_found,
265
+ stage: "error",
266
+ success: !1
267
+ }
268
+ };
269
+ case "broken_connection":
270
+ return {
271
+ type: "broken_connection",
272
+ status: {
273
+ queue: !0,
274
+ message: e.message,
275
+ stage: "error",
276
+ success: !1
277
+ }
278
+ };
279
+ case "estimation":
280
+ return {
281
+ type: "update",
282
+ status: {
283
+ queue: !0,
284
+ stage: s || "pending",
285
+ code: e.code,
286
+ size: e.queue_size,
287
+ position: e.rank,
288
+ eta: e.rank_eta,
289
+ success: e.success
290
+ }
291
+ };
292
+ case "progress":
293
+ return {
294
+ type: "update",
295
+ status: {
296
+ queue: !0,
297
+ stage: "pending",
298
+ code: e.code,
299
+ progress_data: e.progress_data,
300
+ success: e.success
301
+ }
302
+ };
303
+ case "log":
304
+ return { type: "log", data: e };
305
+ case "process_generating":
306
+ return {
307
+ type: "generating",
308
+ status: {
309
+ queue: !0,
310
+ message: e.success ? null : e.output.error,
311
+ stage: e.success ? "generating" : "error",
312
+ code: e.code,
313
+ progress_data: e.progress_data,
314
+ eta: e.average_duration,
315
+ changed_state_ids: e.success ? e.output.changed_state_ids : void 0
316
+ },
317
+ data: e.success ? e.output : null
318
+ };
319
+ case "process_streaming":
320
+ return {
321
+ type: "streaming",
322
+ status: {
323
+ queue: !0,
324
+ message: e.output.error,
325
+ stage: "streaming",
326
+ time_limit: e.time_limit,
327
+ code: e.code,
328
+ progress_data: e.progress_data,
329
+ changed_state_ids: e.output.changed_state_ids,
330
+ eta: e.eta
331
+ },
332
+ data: e.output
333
+ };
334
+ case "process_completed":
335
+ return "error" in e.output ? {
336
+ type: "update",
337
+ status: {
338
+ queue: !0,
339
+ title: e.output.title ?? "Error",
340
+ message: e.output.error ?? "An error occurred",
341
+ visible: e.output.visible,
342
+ duration: e.output.duration,
343
+ stage: "error",
344
+ code: e.code,
345
+ success: e.success
346
+ }
347
+ } : {
348
+ type: "complete",
349
+ status: {
350
+ queue: !0,
351
+ message: e.success ? void 0 : e.output.error,
352
+ stage: e.success ? "complete" : "error",
353
+ code: e.code,
354
+ progress_data: e.progress_data,
355
+ changed_state_ids: e.success ? e.output.changed_state_ids : void 0,
356
+ used_cache: e.used_cache,
357
+ cache_duration: e.cache_duration,
358
+ avg_time: e.avg_time
359
+ },
360
+ data: e.success ? e.output : null
361
+ };
362
+ case "process_starts":
363
+ return {
364
+ type: "update",
365
+ status: {
366
+ queue: !0,
367
+ stage: "pending",
368
+ code: e.code,
369
+ size: e.rank,
370
+ position: 0,
371
+ success: e.success,
372
+ eta: e.eta
373
+ },
374
+ original_msg: "process_starts"
375
+ };
376
+ }
377
+ return { type: "none", status: { stage: "error", queue: !0 } };
378
+ }
379
+ const bt = (e = [], s) => {
380
+ const t = s ? s.parameters : [];
381
+ if (Array.isArray(e))
382
+ return s && t.length > 0 && e.length > t.length && console.warn("Too many arguments provided for the endpoint."), e;
383
+ const n = [], o = Object.keys(e);
384
+ return t.forEach((r, i) => {
385
+ if (e.hasOwnProperty(r.parameter_name))
386
+ n[i] = e[r.parameter_name];
387
+ else if (r.parameter_has_default)
388
+ n[i] = r.parameter_default;
389
+ else
390
+ throw new Error(
391
+ `No value provided for required parameter: ${r.parameter_name}`
392
+ );
393
+ }), o.forEach((r) => {
394
+ if (!t.some((i) => i.parameter_name === r))
395
+ throw new Error(
396
+ `Parameter \`${r}\` is not a valid keyword argument. Please refer to the API for usage.`
397
+ );
398
+ }), n.forEach((r, i) => {
399
+ if (r === void 0 && !t[i].parameter_has_default)
400
+ throw new Error(
401
+ `No value provided for required parameter: ${t[i].parameter_name}`
402
+ );
403
+ }), n;
404
+ };
405
+ async function vt() {
406
+ if (this.api_info) return this.api_info;
407
+ const { token: e } = this.options, { config: s } = this, t = {};
408
+ if (e && (t.Authorization = `Bearer ${e}`), !!s)
409
+ try {
410
+ let n, o;
411
+ if (typeof window < "u" && window.gradio_api_info)
412
+ o = window.gradio_api_info;
413
+ else {
414
+ const r = ae(s.root, this.api_prefix, Qe);
415
+ if (n = await this.fetch(r, {
416
+ headers: t,
417
+ credentials: this.options.credentials ?? "same-origin"
418
+ }), !n.ok)
419
+ throw new Error(N);
420
+ o = await n.json();
421
+ }
422
+ return "api" in o && (o = o.api), o.named_endpoints["/predict"] && !o.unnamed_endpoints[0] && (o.unnamed_endpoints[0] = o.named_endpoints["/predict"]), mt(o, s, this.api_map);
423
+ } catch (n) {
424
+ throw new Error("Could not get API info. " + n.message);
425
+ }
426
+ }
427
+ async function Et(e, s, t) {
428
+ const n = {};
429
+ this?.options?.token && (n.Authorization = `Bearer ${this.options.token}`);
430
+ const o = 1e3, r = [];
431
+ let i;
432
+ for (let a = 0; a < s.length; a += o) {
433
+ const u = s.slice(a, a + o), l = new FormData();
434
+ u.forEach((c) => {
435
+ l.append("files", c);
436
+ });
437
+ try {
438
+ const c = t ? `${e}${this.api_prefix}/${ye}?upload_id=${t}` : `${e}${this.api_prefix}/${ye}`;
439
+ i = await this.fetch(c, {
440
+ method: "POST",
441
+ body: l,
442
+ headers: n,
443
+ credentials: this.options.credentials ?? "same-origin"
444
+ });
445
+ } catch (c) {
446
+ throw new Error(N + c.message);
447
+ }
448
+ if (!i.ok) {
449
+ const c = await i.text();
450
+ return { error: `HTTP ${i.status}: ${c}` };
451
+ }
452
+ const p = await i.json();
453
+ p && r.push(...p);
454
+ }
455
+ return { files: r };
456
+ }
457
+ const St = {
458
+ radix: 1e3,
459
+ unit: ["b", "kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb"]
460
+ }, $t = {
461
+ radix: 1024,
462
+ unit: ["b", "Kib", "Mib", "Gib", "Tib", "Pib", "Eib", "Zib", "Yib"]
463
+ }, kt = {
464
+ radix: 1024,
465
+ unit: ["b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb"]
466
+ }, Se = {
467
+ si: St,
468
+ iec: $t,
469
+ jedec: kt
470
+ };
471
+ function Rt(e, s = 1, t = "jedec") {
472
+ e = Math.abs(e);
473
+ const { radix: n, unit: o } = Se[t] || Se.jedec;
474
+ let r = 0;
475
+ for (; e >= n; )
476
+ e /= n, ++r;
477
+ return `${e.toFixed(s)} ${o[r]}`;
478
+ }
479
+ async function Ot(e, s, t, n) {
480
+ let o = (Array.isArray(e) ? e : [e]).map(
481
+ (i) => i.blob
482
+ );
483
+ const r = o.filter(
484
+ (i) => i.size > (n ?? 1 / 0)
485
+ );
486
+ if (r.length)
487
+ throw new Error(
488
+ `File(s) exceed the maximum allowed size of ${Rt(n || 1 / 0)}: ${r.map((i) => `"${i.name}"`).join(", ")}`
489
+ );
490
+ return await Promise.all(
491
+ await this.upload_files(s, o, t).then(
492
+ async (i) => {
493
+ if (i.error)
494
+ throw new Error(i.error);
495
+ return i.files ? i.files.map((a, u) => new Q({
496
+ ...e[u],
497
+ path: a,
498
+ url: `${s}${this.api_prefix}/file=${a}`
499
+ })) : [];
500
+ }
501
+ )
502
+ );
503
+ }
504
+ async function ns(e, s) {
505
+ return e.map(
506
+ (t) => new Q({
507
+ path: t.name,
508
+ orig_name: t.name,
509
+ blob: t,
510
+ size: t.size,
511
+ mime_type: t.type,
512
+ is_stream: s
513
+ })
514
+ );
515
+ }
516
+ class Q {
517
+ path;
518
+ url;
519
+ orig_name;
520
+ size;
521
+ blob;
522
+ is_stream;
523
+ mime_type;
524
+ alt_text;
525
+ b64;
526
+ meta = { _type: "gradio.FileData" };
527
+ constructor({
528
+ path: s,
529
+ url: t,
530
+ orig_name: n,
531
+ size: o,
532
+ blob: r,
533
+ is_stream: i,
534
+ mime_type: a,
535
+ alt_text: u,
536
+ b64: l
537
+ }) {
538
+ this.path = s, this.url = t, this.orig_name = n, this.size = o, this.blob = t ? void 0 : r, this.is_stream = i, this.mime_type = a, this.alt_text = u, this.b64 = l;
539
+ }
540
+ }
541
+ class Ie {
542
+ type;
543
+ command;
544
+ meta;
545
+ fileData;
546
+ constructor(s, t) {
547
+ this.type = "command", this.command = s, this.meta = t;
548
+ }
549
+ }
550
+ const At = typeof process < "u" && process.versions && process.versions.node;
551
+ function $e(e, s, t) {
552
+ for (; t.length > 1; ) {
553
+ const o = t.shift();
554
+ if (typeof o == "string" || typeof o == "number")
555
+ e = e[o];
556
+ else
557
+ throw new Error("Invalid key type");
558
+ }
559
+ const n = t.shift();
560
+ if (typeof n == "string" || typeof n == "number")
561
+ e[n] = s;
562
+ else
563
+ throw new Error("Invalid key type");
564
+ }
565
+ async function ce(e, s = void 0, t = [], n = !1, o = void 0) {
566
+ if (Array.isArray(e)) {
567
+ let r = [];
568
+ return await Promise.all(
569
+ e.map(async (i, a) => {
570
+ let u = t.slice();
571
+ u.push(String(a));
572
+ const l = await ce(
573
+ e[a],
574
+ n ? o?.parameters[a]?.component || void 0 : s,
575
+ u,
576
+ !1,
577
+ o
578
+ );
579
+ r = r.concat(l);
580
+ })
581
+ ), r;
582
+ } else {
583
+ if (globalThis.Buffer && e instanceof globalThis.Buffer || e instanceof Blob)
584
+ return [
585
+ {
586
+ path: t,
587
+ blob: new Blob([e]),
588
+ type: s
589
+ }
590
+ ];
591
+ if (typeof e == "object" && e !== null) {
592
+ let r = [];
593
+ for (const i of Object.keys(e)) {
594
+ const a = [...t, i], u = e[i];
595
+ r = r.concat(
596
+ await ce(
597
+ u,
598
+ void 0,
599
+ a,
600
+ !1,
601
+ o
602
+ )
603
+ );
604
+ }
605
+ return r;
606
+ }
607
+ }
608
+ return [];
609
+ }
610
+ function Tt(e, s) {
611
+ let t = s?.dependencies?.find((n) => n.id == e)?.queue;
612
+ return t != null ? !t : !s.enable_queue;
613
+ }
614
+ function xt(e, s) {
615
+ return new Promise((t, n) => {
616
+ const o = new MessageChannel();
617
+ o.port1.onmessage = (({ data: r }) => {
618
+ o.port1.close(), t(r);
619
+ }), window.parent.postMessage(e, s, [o.port2]);
620
+ });
621
+ }
622
+ function is(e) {
623
+ if (typeof e == "string") {
624
+ if (e.startsWith("http://") || e.startsWith("https://"))
625
+ return {
626
+ path: e,
627
+ url: e,
628
+ orig_name: e.split("/").pop() ?? "unknown",
629
+ meta: { _type: "gradio.FileData" }
630
+ };
631
+ if (At)
632
+ return new Ie("upload_file", {
633
+ path: e,
634
+ name: e,
635
+ orig_path: e
636
+ });
637
+ } else {
638
+ if (typeof File < "u" && e instanceof File)
639
+ return new Blob([e]);
640
+ if (e instanceof Buffer)
641
+ return new Blob([e]);
642
+ if (e instanceof Blob)
643
+ return e;
644
+ }
645
+ throw new Error(
646
+ "Invalid input: must be a URL, File, Blob, or Buffer object."
647
+ );
648
+ }
649
+ function V(e, s, t, n, o = !1) {
650
+ if (n === "input" && !o)
651
+ throw new Error("Invalid code path. Cannot skip state inputs for input.");
652
+ if (n === "output" && o)
653
+ return e;
654
+ let r = [], i = 0;
655
+ const a = n === "input" ? s.inputs : s.outputs;
656
+ for (let u = 0; u < a.length; u++) {
657
+ const l = a[u];
658
+ if (t.find((c) => c.id === l)?.type === "state") {
659
+ if (o)
660
+ if (e.length === a.length) {
661
+ const c = e[i];
662
+ r.push(c), i++;
663
+ } else
664
+ r.push(null);
665
+ else {
666
+ i++;
667
+ continue;
668
+ }
669
+ continue;
670
+ } else {
671
+ const c = e[i];
672
+ r.push(c), i++;
673
+ }
674
+ }
675
+ return r;
676
+ }
677
+ async function Dt(e, s, t) {
678
+ const n = this;
679
+ await Lt(n, s);
680
+ const o = await ce(
681
+ s,
682
+ void 0,
683
+ [],
684
+ !0,
685
+ t
686
+ );
687
+ return (await Promise.all(
688
+ o.map(async ({ path: i, blob: a, type: u }) => {
689
+ if (!a) return { path: i, type: u };
690
+ const l = await n.upload_files(e, [a]), p = l.files && l.files[0];
691
+ return {
692
+ path: i,
693
+ file_url: p,
694
+ type: u,
695
+ name: typeof File < "u" && a instanceof File ? a?.name : void 0
696
+ };
697
+ })
698
+ )).forEach(({ path: i, file_url: a, type: u, name: l }) => {
699
+ if (u === "Gallery")
700
+ $e(s, a, i);
701
+ else if (a) {
702
+ const p = new Q({ path: a, orig_name: l });
703
+ $e(s, p, i);
704
+ }
705
+ }), s;
706
+ }
707
+ async function Lt(e, s) {
708
+ if (!(e.config?.root || e.config?.root_url))
709
+ throw new Error(Ne);
710
+ await Ue(e, s);
711
+ }
712
+ async function Ue(e, s, t = []) {
713
+ for (const n in s)
714
+ s[n] instanceof Ie ? await Nt(e, s, n) : typeof s[n] == "object" && s[n] !== null && await Ue(e, s[n], [...t, n]);
715
+ }
716
+ async function Nt(e, s, t) {
717
+ let n = s[t];
718
+ const o = e.config?.root || e.config?.root_url;
719
+ if (!o)
720
+ throw new Error(Ne);
721
+ try {
722
+ let r, i;
723
+ if (typeof process < "u" && process.versions && process.versions.node) {
724
+ const p = await import("./__vite-browser-external-DYxpcVy9.js");
725
+ i = (await import("./__vite-browser-external-DYxpcVy9.js")).resolve(process.cwd(), n.meta.path), r = await p.readFile(i);
726
+ } else
727
+ throw new Error(pt);
728
+ const a = new Blob([r], {
729
+ type: "application/octet-stream"
730
+ }), u = await e.upload_files(o, [a]), l = u.files && u.files[0];
731
+ if (l) {
732
+ const p = new Q({
733
+ path: l,
734
+ orig_name: n.meta.name || ""
735
+ });
736
+ s[t] = p;
737
+ }
738
+ } catch (r) {
739
+ console.error(dt, r);
740
+ }
741
+ }
742
+ async function Ct(e, s, t) {
743
+ const n = { "Content-Type": "application/json" };
744
+ this.options.token && (n.Authorization = `Bearer ${this.options.token}`);
745
+ try {
746
+ var o = await this.fetch(e, {
747
+ method: "POST",
748
+ body: JSON.stringify(s),
749
+ headers: { ...n, ...t },
750
+ credentials: this.options.credentials ?? "same-origin"
751
+ });
752
+ } catch {
753
+ return [{ error: N }, 500];
754
+ }
755
+ let r, i;
756
+ try {
757
+ r = await o.json(), i = o.status;
758
+ } catch (a) {
759
+ r = { error: `Could not parse server response: ${a}` }, i = 500;
760
+ }
761
+ return [r, i];
762
+ }
763
+ async function Pt(e, s = {}) {
764
+ let t = !1, n = !1;
765
+ if (!this.config)
766
+ throw new Error("Could not resolve app config");
767
+ if (typeof e == "number")
768
+ this.config.dependencies.find((i) => i.id == e);
769
+ else {
770
+ const i = e.replace(/^\//, "");
771
+ this.config.dependencies.find(
772
+ (a) => a.id == this.api_map[i]
773
+ );
774
+ }
775
+ const o = this.submit(e, s, null, null, !0);
776
+ let r;
777
+ for await (const i of o) {
778
+ if (i.type === "data" && (t = !0, r = i, n))
779
+ return r;
780
+ if (i.type === "status") {
781
+ if (i.stage === "error")
782
+ throw i;
783
+ if (i.stage === "complete" && (n = !0, t))
784
+ return r;
785
+ }
786
+ }
787
+ return r;
788
+ }
789
+ async function F(e, s, t) {
790
+ let n = s === "subdomain" ? `https://huggingface.co/api/spaces/by-subdomain/${e}` : `https://huggingface.co/api/spaces/${e}`, o, r;
791
+ try {
792
+ if (o = await fetch(n), r = o.status, r !== 200)
793
+ throw new Error();
794
+ o = await o.json();
795
+ } catch {
796
+ t({
797
+ status: "error",
798
+ load_status: "error",
799
+ message: rt,
800
+ detail: "NOT_FOUND"
801
+ });
802
+ return;
803
+ }
804
+ if (!o || r !== 200) return;
805
+ const {
806
+ runtime: { stage: i },
807
+ id: a
808
+ } = o;
809
+ switch (i) {
810
+ case "STOPPED":
811
+ case "SLEEPING":
812
+ t({
813
+ status: "sleeping",
814
+ load_status: "pending",
815
+ message: "Space is asleep. Waking it up...",
816
+ detail: i
817
+ }), setTimeout(() => {
818
+ F(e, s, t);
819
+ }, 1e3);
820
+ break;
821
+ case "PAUSED":
822
+ t({
823
+ status: "paused",
824
+ load_status: "error",
825
+ message: "This space has been paused by the author. If you would like to try this demo, consider duplicating the space.",
826
+ detail: i,
827
+ discussions_enabled: await ke(a)
828
+ });
829
+ break;
830
+ case "RUNNING":
831
+ case "RUNNING_BUILDING":
832
+ t({
833
+ status: "running",
834
+ load_status: "complete",
835
+ message: "Space is running.",
836
+ detail: i
837
+ });
838
+ break;
839
+ case "BUILDING":
840
+ t({
841
+ status: "building",
842
+ load_status: "pending",
843
+ message: "Space is building...",
844
+ detail: i
845
+ }), setTimeout(() => {
846
+ F(e, s, t);
847
+ }, 1e3);
848
+ break;
849
+ case "APP_STARTING":
850
+ t({
851
+ status: "starting",
852
+ load_status: "pending",
853
+ message: "Space is starting...",
854
+ detail: i
855
+ }), setTimeout(() => {
856
+ F(e, s, t);
857
+ }, 1e3);
858
+ break;
859
+ default:
860
+ t({
861
+ status: "space_error",
862
+ load_status: "error",
863
+ message: "This space is experiencing an issue.",
864
+ detail: i,
865
+ discussions_enabled: await ke(a)
866
+ });
867
+ break;
868
+ }
869
+ }
870
+ const je = async (e, s) => {
871
+ let t = 0;
872
+ const n = 12, o = 5e3;
873
+ return new Promise((r) => {
874
+ F(
875
+ e,
876
+ le.test(e) ? "space_name" : "subdomain",
877
+ (i) => {
878
+ s(i), i.status === "running" || i.status === "error" || i.status === "paused" || i.status === "space_error" ? r() : (i.status === "sleeping" || i.status === "building") && (t < n ? (t++, setTimeout(() => {
879
+ je(e, s).then(r);
880
+ }, o)) : r());
881
+ }
882
+ );
883
+ });
884
+ }, It = /^(?=[^]*\b[dD]iscussions{0,1}\b)(?=[^]*\b[dD]isabled\b)[^]*$/;
885
+ async function ke(e) {
886
+ try {
887
+ const s = await fetch(
888
+ `https://huggingface.co/api/spaces/${e}/discussions`,
889
+ {
890
+ method: "HEAD"
891
+ }
892
+ ), t = s.headers.get("x-error-message");
893
+ return !(!s.ok || t && It.test(t));
894
+ } catch {
895
+ return !1;
896
+ }
897
+ }
898
+ async function Ut(e, s) {
899
+ const t = {};
900
+ s && (t.Authorization = `Bearer ${s}`);
901
+ try {
902
+ const n = await fetch(
903
+ `https://huggingface.co/api/spaces/${e}/${Xe}`,
904
+ { headers: t }
905
+ );
906
+ if (n.status !== 200)
907
+ throw new Error("Space hardware could not be obtained.");
908
+ const { hardware: o } = await n.json();
909
+ return o.current;
910
+ } catch (n) {
911
+ throw new Error(n.message);
912
+ }
913
+ }
914
+ async function jt(e, s, t) {
915
+ const n = {};
916
+ t && (n.Authorization = `Bearer ${t}`);
917
+ const o = {
918
+ seconds: s
919
+ };
920
+ try {
921
+ const r = await fetch(
922
+ `https://huggingface.co/api/spaces/${e}/${et}`,
923
+ {
924
+ method: "POST",
925
+ headers: { "Content-Type": "application/json", ...n },
926
+ body: JSON.stringify(o)
927
+ }
928
+ );
929
+ if (r.status !== 200)
930
+ throw new Error(
931
+ "Could not set sleep timeout on duplicated Space. Please visit *ADD HF LINK TO SETTINGS* to set a timeout manually to reduce billing charges."
932
+ );
933
+ return await r.json();
934
+ } catch (r) {
935
+ throw new Error(r.message);
936
+ }
937
+ }
938
+ const Re = [
939
+ "cpu-basic",
940
+ "cpu-upgrade",
941
+ "cpu-xl",
942
+ "t4-small",
943
+ "t4-medium",
944
+ "a10g-small",
945
+ "a10g-large",
946
+ "a10g-largex2",
947
+ "a10g-largex4",
948
+ "a100-large",
949
+ "zero-a10g",
950
+ "h100",
951
+ "h100x8"
952
+ ];
953
+ async function qt(e, s) {
954
+ const { token: t, private: n, hardware: o, timeout: r, auth: i } = s;
955
+ if (o && !Re.includes(o))
956
+ throw new Error(
957
+ `Invalid hardware type provided. Valid types are: ${Re.map((m) => `"${m}"`).join(",")}.`
958
+ );
959
+ const { http_protocol: a, host: u } = await pe(e, t);
960
+ let l = null;
961
+ if (i) {
962
+ const m = await Ce(
963
+ a,
964
+ u,
965
+ i,
966
+ fetch,
967
+ void 0,
968
+ s.credentials
969
+ );
970
+ m && (l = Pe(m));
971
+ }
972
+ const p = {
973
+ Authorization: `Bearer ${t}`,
974
+ "Content-Type": "application/json",
975
+ ...l ? { Cookie: l.join("; ") } : {}
976
+ }, c = (await (await fetch("https://huggingface.co/api/whoami-v2", {
977
+ headers: p
978
+ })).json()).name, g = e.split("/")[1], w = {
979
+ repository: `${c}/${g}`
980
+ };
981
+ n && (w.private = !0);
982
+ let C;
983
+ try {
984
+ o || (C = await Ut(e, t));
985
+ } catch (m) {
986
+ throw Error(ue + m.message);
987
+ }
988
+ const f = o || C || "cpu-basic";
989
+ w.hardware = f;
990
+ try {
991
+ const m = await fetch(
992
+ `https://huggingface.co/api/spaces/${e}/duplicate`,
993
+ {
994
+ method: "POST",
995
+ headers: p,
996
+ body: JSON.stringify(w)
997
+ }
998
+ );
999
+ if (m.status === 409)
1000
+ try {
1001
+ return await Y.connect(`${c}/${g}`, s);
1002
+ } catch (z) {
1003
+ throw console.error("Failed to connect Client instance:", z), z;
1004
+ }
1005
+ else if (m.status !== 200)
1006
+ throw new Error(m.statusText);
1007
+ const H = await m.json();
1008
+ return await jt(`${c}/${g}`, r || 300, t), await Y.connect(
1009
+ zt(H.url),
1010
+ s
1011
+ );
1012
+ } catch (m) {
1013
+ throw new Error(m);
1014
+ }
1015
+ }
1016
+ function zt(e) {
1017
+ const s = /https:\/\/huggingface.co\/spaces\/([^/]+\/[^/]+)/, t = e.match(s);
1018
+ if (t)
1019
+ return t[1];
1020
+ }
1021
+ const Oe = "supports-zerogpu-headers";
1022
+ let Ae = !1;
1023
+ function Bt() {
1024
+ return typeof window < "u" && typeof document < "u" && typeof window.addEventListener == "function";
1025
+ }
1026
+ function qe(e) {
1027
+ return e.includes(".dev.") ? `https://moon-${e.split(".")[1]}.dev.spaces.huggingface.tech` : e.endsWith(".hf.space") ? "https://huggingface.co" : null;
1028
+ }
1029
+ function Gt() {
1030
+ if (!Bt() || Ae)
1031
+ return;
1032
+ window.addEventListener("message", (s) => {
1033
+ s.data === Oe && (window.supports_zerogpu_headers = !0);
1034
+ }), Ae = !0;
1035
+ const e = qe(window.location.hostname);
1036
+ e && window.parent !== window && window.parent.postMessage(Oe, e);
1037
+ }
1038
+ class Mt extends TransformStream {
1039
+ #e = "";
1040
+ /** Constructs a new instance. */
1041
+ constructor(s = { allowCR: !1 }) {
1042
+ super({
1043
+ transform: (t, n) => {
1044
+ for (t = this.#e + t; ; ) {
1045
+ const o = t.indexOf(`
1046
+ `), r = s.allowCR ? t.indexOf("\r") : -1;
1047
+ if (r !== -1 && r !== t.length - 1 && (o === -1 || o - 1 > r)) {
1048
+ n.enqueue(t.slice(0, r)), t = t.slice(r + 1);
1049
+ continue;
1050
+ }
1051
+ if (o === -1)
1052
+ break;
1053
+ const i = t[o - 1] === "\r" ? o - 1 : o;
1054
+ n.enqueue(t.slice(0, i)), t = t.slice(o + 1);
1055
+ }
1056
+ this.#e = t;
1057
+ },
1058
+ flush: (t) => {
1059
+ if (this.#e === "")
1060
+ return;
1061
+ const n = s.allowCR && this.#e.endsWith("\r") ? this.#e.slice(0, -1) : this.#e;
1062
+ t.enqueue(n);
1063
+ }
1064
+ });
1065
+ }
1066
+ }
1067
+ function Ft(e) {
1068
+ let s = new TextDecoderStream(), t = new Mt({ allowCR: !0 });
1069
+ return e.pipeThrough(s).pipeThrough(t);
1070
+ }
1071
+ function Ht(e) {
1072
+ let t = /[:]\s*/.exec(e), n = t && t.index;
1073
+ if (n)
1074
+ return [
1075
+ e.substring(0, n),
1076
+ e.substring(n + t[0].length)
1077
+ ];
1078
+ }
1079
+ function Te(e, s, t) {
1080
+ e.get(s) || e.set(s, t);
1081
+ }
1082
+ async function* Jt(e, s) {
1083
+ if (!e.body)
1084
+ return;
1085
+ let t = Ft(e.body), n, o = t.getReader(), r;
1086
+ for (; ; ) {
1087
+ if (s && s.aborted)
1088
+ return o.cancel();
1089
+ if (n = await o.read(), n.done)
1090
+ return;
1091
+ if (!n.value) {
1092
+ r && (yield r), r = void 0;
1093
+ continue;
1094
+ }
1095
+ let [i, a] = Ht(n.value) || [];
1096
+ i === "data" ? (r ||= {}, r[i] = r[i] ? r[i] + `
1097
+ ` + a : a) : i === "event" ? (r ||= {}, r[i] = a) : i === "id" ? (r ||= {}, r[i] = String(+a) === a ? +a : a) : i === "retry" && (r ||= {}, r[i] = +a || void 0);
1098
+ }
1099
+ }
1100
+ async function Wt(e, s) {
1101
+ let t = new Request(e, s);
1102
+ Te(t.headers, "Accept", "text/event-stream"), Te(t.headers, "Content-Type", "application/json");
1103
+ let n = await fetch(t);
1104
+ if (!n.ok)
1105
+ throw n;
1106
+ return Jt(n, t.signal);
1107
+ }
1108
+ async function Zt() {
1109
+ let {
1110
+ event_callbacks: e,
1111
+ unclosed_events: s,
1112
+ pending_stream_messages: t,
1113
+ stream_status: n,
1114
+ config: o,
1115
+ jwt: r
1116
+ } = this;
1117
+ const i = this;
1118
+ if (!o)
1119
+ throw new Error("Could not resolve app config");
1120
+ n.open = !0;
1121
+ let a = null, u = new URLSearchParams({
1122
+ session_hash: this.session_hash
1123
+ }).toString(), l = new URL(`${o.root}${this.api_prefix}/${xe}?${u}`);
1124
+ if (r && l.searchParams.set("__sign", r), a = this.stream(l), !a) {
1125
+ console.warn("Cannot connect to SSE endpoint: " + l.toString());
1126
+ return;
1127
+ }
1128
+ a.onmessage = async function(p) {
1129
+ let c = JSON.parse(p.data);
1130
+ if (c.msg === "close_stream") {
1131
+ de(n, i.abort_controller);
1132
+ return;
1133
+ }
1134
+ const g = c.event_id;
1135
+ if (!g)
1136
+ await Promise.all(
1137
+ Object.keys(e).map(
1138
+ (w) => e[w](c)
1139
+ )
1140
+ );
1141
+ else if (e[g] && o) {
1142
+ c.msg === "process_completed" && ["sse", "sse_v1", "sse_v2", "sse_v2.1", "sse_v3"].includes(
1143
+ o.protocol
1144
+ ) && s.delete(g);
1145
+ let w = e[g];
1146
+ typeof window < "u" && typeof document < "u" ? setTimeout(w, 0, c) : w(c);
1147
+ } else
1148
+ t[g] || (t[g] = []), t[g].push(c);
1149
+ }, a.onerror = async function(p) {
1150
+ console.error(p), await Promise.all(
1151
+ Object.keys(e).map(
1152
+ (c) => e[c]({
1153
+ msg: "broken_connection",
1154
+ message: N
1155
+ })
1156
+ )
1157
+ );
1158
+ };
1159
+ }
1160
+ function de(e, s) {
1161
+ e && (e.open = !1, s?.abort());
1162
+ }
1163
+ function Kt(e, s, t) {
1164
+ !e[s] ? (e[s] = [], t.data.forEach((o, r) => {
1165
+ e[s][r] = o;
1166
+ })) : t.data.forEach((o, r) => {
1167
+ let i = Vt(e[s][r], o);
1168
+ e[s][r] = i, t.data[r] = i;
1169
+ });
1170
+ }
1171
+ function Vt(e, s) {
1172
+ return s.forEach(([t, n, o]) => {
1173
+ e = Yt(e, n, t, o);
1174
+ }), e;
1175
+ }
1176
+ function Yt(e, s, t, n) {
1177
+ if (s.length === 0) {
1178
+ if (t === "replace")
1179
+ return n;
1180
+ if (t === "append")
1181
+ return e + n;
1182
+ throw new Error(`Unsupported action: ${t}`);
1183
+ }
1184
+ let o = e;
1185
+ for (let i = 0; i < s.length - 1; i++)
1186
+ o = o[s[i]];
1187
+ const r = s[s.length - 1];
1188
+ switch (t) {
1189
+ case "replace":
1190
+ o[r] = n;
1191
+ break;
1192
+ case "append":
1193
+ o[r] += n;
1194
+ break;
1195
+ case "add":
1196
+ Array.isArray(o) ? o.splice(Number(r), 0, n) : o[r] = n;
1197
+ break;
1198
+ case "delete":
1199
+ Array.isArray(o) ? o.splice(Number(r), 1) : delete o[r];
1200
+ break;
1201
+ default:
1202
+ throw new Error(`Unknown action: ${t}`);
1203
+ }
1204
+ return e;
1205
+ }
1206
+ function Qt(e, s = {}) {
1207
+ const t = {
1208
+ close: () => {
1209
+ console.warn("Method not implemented.");
1210
+ },
1211
+ onerror: null,
1212
+ onmessage: null,
1213
+ onopen: null,
1214
+ readyState: 0,
1215
+ url: e.toString(),
1216
+ withCredentials: !1,
1217
+ CONNECTING: 0,
1218
+ OPEN: 1,
1219
+ CLOSED: 2,
1220
+ addEventListener: () => {
1221
+ throw new Error("Method not implemented.");
1222
+ },
1223
+ dispatchEvent: () => {
1224
+ throw new Error("Method not implemented.");
1225
+ },
1226
+ removeEventListener: () => {
1227
+ throw new Error("Method not implemented.");
1228
+ }
1229
+ };
1230
+ return Wt(e, s).then(async (n) => {
1231
+ t.readyState = t.OPEN;
1232
+ try {
1233
+ for await (const o of n)
1234
+ t.onmessage && t.onmessage(o);
1235
+ t.readyState = t.CLOSED;
1236
+ } catch (o) {
1237
+ t.onerror && t.onerror(o), t.readyState = t.CLOSED;
1238
+ }
1239
+ }).catch((n) => {
1240
+ console.error(n), t.onerror && t.onerror(n), t.readyState = t.CLOSED;
1241
+ }), t;
1242
+ }
1243
+ function Xt(e, s = {}, t, n, o, r) {
1244
+ try {
1245
+ let i = function(h) {
1246
+ (o || Me[h.type]) && p(h);
1247
+ }, a = function() {
1248
+ for (me = !0; M.length > 0; )
1249
+ M.shift()({
1250
+ value: void 0,
1251
+ done: !0
1252
+ });
1253
+ }, u = function(h) {
1254
+ M.length > 0 ? M.shift()(h) : ie.push(h);
1255
+ }, l = function(h) {
1256
+ u(es(h)), a();
1257
+ }, p = function(h) {
1258
+ u({ value: h, done: !1 });
1259
+ }, c = function() {
1260
+ return ie.length > 0 ? Promise.resolve(ie.shift()) : me ? Promise.resolve({ value: void 0, done: !0 }) : new Promise((h) => M.push(h));
1261
+ };
1262
+ const { token: g } = this.options, {
1263
+ fetch: w,
1264
+ app_reference: C,
1265
+ config: f,
1266
+ session_hash: m,
1267
+ api_info: H,
1268
+ api_map: z,
1269
+ stream_status: X,
1270
+ pending_stream_messages: ee,
1271
+ pending_diff_streams: te,
1272
+ event_callbacks: se,
1273
+ unclosed_events: ze,
1274
+ post_data: ne,
1275
+ options: J,
1276
+ api_prefix: A
1277
+ } = this, he = r || { "x-gradio-user": "api" }, Be = this;
1278
+ if (!H) throw new Error("No API found");
1279
+ if (!f) throw new Error("Could not resolve app config");
1280
+ let { fn_index: d, endpoint_info: fe, dependency: B } = ts(
1281
+ H,
1282
+ e,
1283
+ z,
1284
+ f
1285
+ ), Ge = bt(s, fe), I, T = f.protocol ?? "ws";
1286
+ if (T === "ws")
1287
+ throw new Error("WebSocket protocol is not supported in this version");
1288
+ let U = "", ss = () => U;
1289
+ const _ = typeof e == "number" ? "/predict" : e;
1290
+ let W, v = null, x = !1, _e = {}, G = typeof window < "u" && typeof document < "u" ? new URLSearchParams(window.location.search).toString() : "";
1291
+ const Me = J?.events?.reduce(
1292
+ (h, R) => (h[R] = !0, h),
1293
+ {}
1294
+ ) || {};
1295
+ async function Fe() {
1296
+ let h = {}, R = {};
1297
+ h = { event_id: v }, R = { event_id: v, session_hash: m, fn_index: d };
1298
+ try {
1299
+ if (!f)
1300
+ throw new Error("Could not resolve app config");
1301
+ "event_id" in R && await w(`${f.root}${A}/${it}`, {
1302
+ headers: { "Content-Type": "application/json" },
1303
+ method: "POST",
1304
+ body: JSON.stringify(R)
1305
+ }), await w(`${f.root}${A}/${nt}`, {
1306
+ headers: { "Content-Type": "application/json" },
1307
+ method: "POST",
1308
+ body: JSON.stringify(h)
1309
+ });
1310
+ } catch {
1311
+ console.warn(
1312
+ "The `/reset` endpoint could not be called. Subsequent endpoint results may be unreliable."
1313
+ );
1314
+ }
1315
+ }
1316
+ const He = async (h) => {
1317
+ await this._resolve_heartbeat(h);
1318
+ };
1319
+ async function ge(h) {
1320
+ if (!f) return;
1321
+ let R = h.render_id;
1322
+ f.components = [
1323
+ ...f.components.filter((E) => E.props.rendered_in !== R),
1324
+ ...h.components
1325
+ ], f.dependencies = [
1326
+ ...f.dependencies.filter((E) => E.rendered_in !== R),
1327
+ ...h.dependencies
1328
+ ];
1329
+ const Z = f.components.some((E) => E.type === "state"), y = f.dependencies.some(
1330
+ (E) => E.targets.some((j) => j[1] === "unload")
1331
+ );
1332
+ f.connect_heartbeat = Z || y, await He(f), i({
1333
+ type: "render",
1334
+ data: h,
1335
+ endpoint: _,
1336
+ fn_index: d
1337
+ });
1338
+ }
1339
+ const Je = this.handle_blob(
1340
+ f.root,
1341
+ Ge,
1342
+ fe
1343
+ ).then(async (h) => {
1344
+ if (W = {
1345
+ data: V(
1346
+ h,
1347
+ B,
1348
+ f.components,
1349
+ "input",
1350
+ !0
1351
+ ) || [],
1352
+ event_data: t,
1353
+ fn_index: d,
1354
+ trigger_id: n
1355
+ }, Tt(d, f))
1356
+ i({
1357
+ type: "status",
1358
+ endpoint: _,
1359
+ stage: "pending",
1360
+ queue: !1,
1361
+ fn_index: d,
1362
+ time: /* @__PURE__ */ new Date()
1363
+ }), ne(
1364
+ `${f.root}${A}/run${_.startsWith("/") ? _ : `/${_}`}${G ? "?" + G : ""}`,
1365
+ {
1366
+ ...W,
1367
+ session_hash: m
1368
+ },
1369
+ he
1370
+ ).then(async ([y, E]) => {
1371
+ const j = y.data;
1372
+ if (E == 200)
1373
+ i({
1374
+ type: "data",
1375
+ endpoint: _,
1376
+ fn_index: d,
1377
+ data: V(
1378
+ j,
1379
+ B,
1380
+ f.components,
1381
+ "output",
1382
+ J.with_null_state
1383
+ ),
1384
+ time: /* @__PURE__ */ new Date(),
1385
+ event_data: t,
1386
+ trigger_id: n
1387
+ }), y.render_config && await ge(y.render_config), i({
1388
+ type: "status",
1389
+ endpoint: _,
1390
+ fn_index: d,
1391
+ stage: "complete",
1392
+ eta: y.average_duration,
1393
+ queue: !1,
1394
+ time: /* @__PURE__ */ new Date()
1395
+ });
1396
+ else {
1397
+ const O = y?.error === N;
1398
+ i({
1399
+ type: "status",
1400
+ stage: "error",
1401
+ endpoint: _,
1402
+ fn_index: d,
1403
+ message: y.error,
1404
+ broken: O,
1405
+ queue: !1,
1406
+ time: /* @__PURE__ */ new Date()
1407
+ });
1408
+ }
1409
+ }).catch((y) => {
1410
+ i({
1411
+ type: "status",
1412
+ stage: "error",
1413
+ message: y.message,
1414
+ endpoint: _,
1415
+ fn_index: d,
1416
+ queue: !1,
1417
+ time: /* @__PURE__ */ new Date()
1418
+ });
1419
+ });
1420
+ else if (T == "sse") {
1421
+ i({
1422
+ type: "status",
1423
+ stage: "pending",
1424
+ queue: !0,
1425
+ endpoint: _,
1426
+ fn_index: d,
1427
+ time: /* @__PURE__ */ new Date()
1428
+ });
1429
+ var Z = new URLSearchParams({
1430
+ fn_index: d.toString(),
1431
+ session_hash: m
1432
+ }).toString();
1433
+ let y = new URL(
1434
+ `${f.root}${A}/${xe}?${G ? G + "&" : ""}${Z}`
1435
+ );
1436
+ if (this.jwt && y.searchParams.set("__sign", this.jwt), I = this.stream(y), !I)
1437
+ return Promise.reject(
1438
+ new Error("Cannot connect to SSE endpoint: " + y.toString())
1439
+ );
1440
+ I.onmessage = async function(E) {
1441
+ const j = JSON.parse(E.data), { type: O, status: D, data: b } = Ee(
1442
+ j,
1443
+ _e[d]
1444
+ );
1445
+ if (O === "update" && D && !x)
1446
+ i({
1447
+ type: "status",
1448
+ endpoint: _,
1449
+ fn_index: d,
1450
+ time: /* @__PURE__ */ new Date(),
1451
+ ...D
1452
+ }), D.stage === "error" && (I?.close(), a());
1453
+ else if (O === "data") {
1454
+ let [q, P] = await ne(
1455
+ `${f.root}${A}/queue/data`,
1456
+ {
1457
+ ...W,
1458
+ session_hash: m,
1459
+ event_id: v
1460
+ }
1461
+ );
1462
+ P !== 200 && (i({
1463
+ type: "status",
1464
+ stage: "error",
1465
+ message: N,
1466
+ queue: !0,
1467
+ endpoint: _,
1468
+ fn_index: d,
1469
+ time: /* @__PURE__ */ new Date()
1470
+ }), I?.close(), a());
1471
+ } else O === "complete" ? x = D : O === "log" ? i({
1472
+ type: "log",
1473
+ title: b.title,
1474
+ log: b.log,
1475
+ level: b.level,
1476
+ endpoint: _,
1477
+ duration: b.duration,
1478
+ visible: b.visible,
1479
+ fn_index: d
1480
+ }) : (O === "generating" || O === "streaming") && i({
1481
+ type: "status",
1482
+ time: /* @__PURE__ */ new Date(),
1483
+ ...D,
1484
+ stage: D?.stage,
1485
+ queue: !0,
1486
+ endpoint: _,
1487
+ fn_index: d
1488
+ });
1489
+ b && (i({
1490
+ type: "data",
1491
+ time: /* @__PURE__ */ new Date(),
1492
+ data: V(
1493
+ b.data,
1494
+ B,
1495
+ f.components,
1496
+ "output",
1497
+ J.with_null_state
1498
+ ),
1499
+ endpoint: _,
1500
+ fn_index: d,
1501
+ event_data: t,
1502
+ trigger_id: n
1503
+ }), x && (i({
1504
+ type: "status",
1505
+ time: /* @__PURE__ */ new Date(),
1506
+ ...x,
1507
+ stage: D?.stage,
1508
+ queue: !0,
1509
+ endpoint: _,
1510
+ fn_index: d
1511
+ }), I?.close(), a()));
1512
+ };
1513
+ } else if (T == "sse_v1" || T == "sse_v2" || T == "sse_v2.1" || T == "sse_v3") {
1514
+ i({
1515
+ type: "status",
1516
+ stage: "pending",
1517
+ queue: !0,
1518
+ endpoint: _,
1519
+ fn_index: d,
1520
+ time: /* @__PURE__ */ new Date()
1521
+ });
1522
+ let y = "";
1523
+ typeof window < "u" && typeof document < "u" && (y = window?.location?.hostname);
1524
+ const E = qe(y);
1525
+ return (typeof window < "u" && typeof document < "u" && window.parent != window && !!E && window.supports_zerogpu_headers ? xt("zerogpu-headers", E) : Promise.resolve(null)).then((b) => {
1526
+ const q = { ...he, ...b || {} };
1527
+ return ne(
1528
+ `${f.root}${A}/${Ve}?${G}`,
1529
+ {
1530
+ ...W,
1531
+ session_hash: m
1532
+ },
1533
+ q
1534
+ );
1535
+ }).then(async ([b, q]) => {
1536
+ if (b.event_id && (U = b.event_id), q === 503)
1537
+ i({
1538
+ type: "status",
1539
+ stage: "error",
1540
+ message: De,
1541
+ queue: !0,
1542
+ endpoint: _,
1543
+ fn_index: d,
1544
+ time: /* @__PURE__ */ new Date(),
1545
+ visible: !0
1546
+ }), a();
1547
+ else if (q === 422)
1548
+ i({
1549
+ type: "status",
1550
+ stage: "error",
1551
+ message: b.detail,
1552
+ queue: !0,
1553
+ endpoint: _,
1554
+ fn_index: d,
1555
+ code: "validation_error",
1556
+ time: /* @__PURE__ */ new Date(),
1557
+ visible: !0
1558
+ }), a();
1559
+ else if (q !== 200) {
1560
+ const P = b?.error === N;
1561
+ i({
1562
+ type: "status",
1563
+ stage: "error",
1564
+ broken: P,
1565
+ message: P ? N : b.detail || b.error,
1566
+ queue: !0,
1567
+ endpoint: _,
1568
+ fn_index: d,
1569
+ time: /* @__PURE__ */ new Date(),
1570
+ visible: !0
1571
+ }), a();
1572
+ } else {
1573
+ v = b.event_id, U = v;
1574
+ let P = async function(oe) {
1575
+ try {
1576
+ const { type: S, status: $, data: k, original_msg: We } = Ee(
1577
+ oe,
1578
+ _e[d]
1579
+ );
1580
+ if (S == "heartbeat")
1581
+ return;
1582
+ if (S === "update" && $ && !x)
1583
+ i({
1584
+ type: "status",
1585
+ endpoint: _,
1586
+ fn_index: d,
1587
+ time: /* @__PURE__ */ new Date(),
1588
+ original_msg: We,
1589
+ ...$
1590
+ });
1591
+ else if (S === "complete")
1592
+ x = $;
1593
+ else if (S == "unexpected_error" || S == "broken_connection") {
1594
+ console.error("Unexpected error", $?.message);
1595
+ const Ze = S === "broken_connection";
1596
+ i({
1597
+ type: "status",
1598
+ stage: "error",
1599
+ message: $?.message || "An Unexpected Error Occurred!",
1600
+ queue: !0,
1601
+ endpoint: _,
1602
+ broken: Ze,
1603
+ session_not_found: $?.session_not_found,
1604
+ fn_index: d,
1605
+ time: /* @__PURE__ */ new Date()
1606
+ });
1607
+ } else if (S === "log") {
1608
+ i({
1609
+ type: "log",
1610
+ title: k.title,
1611
+ log: k.log,
1612
+ level: k.level,
1613
+ endpoint: _,
1614
+ duration: k.duration,
1615
+ visible: k.visible,
1616
+ fn_index: d
1617
+ });
1618
+ return;
1619
+ } else (S === "generating" || S === "streaming") && (i({
1620
+ type: "status",
1621
+ time: /* @__PURE__ */ new Date(),
1622
+ ...$,
1623
+ stage: $?.stage,
1624
+ queue: !0,
1625
+ endpoint: _,
1626
+ fn_index: d
1627
+ }), k && B.connection !== "stream" && ["sse_v2", "sse_v2.1", "sse_v3"].includes(T) && Kt(te, v, k));
1628
+ k && (i({
1629
+ type: "data",
1630
+ time: /* @__PURE__ */ new Date(),
1631
+ data: V(
1632
+ k.data,
1633
+ B,
1634
+ f.components,
1635
+ "output",
1636
+ J.with_null_state
1637
+ ),
1638
+ endpoint: _,
1639
+ fn_index: d
1640
+ }), k.render_config && await ge(k.render_config), x && (i({
1641
+ type: "status",
1642
+ time: /* @__PURE__ */ new Date(),
1643
+ ...x,
1644
+ stage: $?.stage,
1645
+ queue: !0,
1646
+ endpoint: _,
1647
+ fn_index: d
1648
+ }), a())), ($?.stage === "complete" || $?.stage === "error") && (se[v] && delete se[v], v in te && delete te[v], a());
1649
+ } catch (S) {
1650
+ console.error("Unexpected client exception", S), i({
1651
+ type: "status",
1652
+ stage: "error",
1653
+ message: "An Unexpected Error Occurred!",
1654
+ queue: !0,
1655
+ endpoint: _,
1656
+ fn_index: d,
1657
+ time: /* @__PURE__ */ new Date()
1658
+ }), ["sse_v2", "sse_v2.1", "sse_v3"].includes(T) && (de(X, Be.abort_controller), X.open = !1, a());
1659
+ }
1660
+ };
1661
+ v in ee && (ee[v].forEach((oe) => P(oe)), delete ee[v]), se[v] = P, ze.add(v), X.open || await this.open_stream();
1662
+ }
1663
+ });
1664
+ }
1665
+ });
1666
+ let me = !1;
1667
+ const ie = [], M = [], we = {
1668
+ [Symbol.asyncIterator]: () => we,
1669
+ next: c,
1670
+ throw: async (h) => (l(h), c()),
1671
+ return: async () => (a(), { value: void 0, done: !0 }),
1672
+ cancel: Fe,
1673
+ send_chunk: (h) => {
1674
+ this.post_data(`${f.root}${A}/stream/${U}`, {
1675
+ ...h,
1676
+ session_hash: this.session_hash
1677
+ });
1678
+ },
1679
+ close_stream: () => {
1680
+ this.post_data(
1681
+ `${f.root}${A}/stream/${U}/close`,
1682
+ {}
1683
+ ), a();
1684
+ },
1685
+ event_id: () => U,
1686
+ wait_for_id: async () => (await Je, v)
1687
+ };
1688
+ return we;
1689
+ } catch (i) {
1690
+ throw console.error("Submit function encountered an error:", i), i;
1691
+ }
1692
+ }
1693
+ function es(e) {
1694
+ return {
1695
+ then: (s, t) => t(e)
1696
+ };
1697
+ }
1698
+ function ts(e, s, t, n) {
1699
+ let o, r, i;
1700
+ if (typeof s == "number")
1701
+ o = s, r = e.unnamed_endpoints[o], i = n.dependencies.find((a) => a.id == s);
1702
+ else {
1703
+ const a = s.replace(/^\//, "");
1704
+ o = t[a], r = e.named_endpoints[s.trim()], i = n.dependencies.find(
1705
+ (u) => u.id == t[a]
1706
+ );
1707
+ }
1708
+ if (typeof o != "number")
1709
+ throw new Error(
1710
+ "There is no endpoint matching that name of fn_index matching that number."
1711
+ );
1712
+ return { fn_index: o, endpoint_info: r, dependency: i };
1713
+ }
1714
+ class Y {
1715
+ app_reference;
1716
+ options;
1717
+ deep_link = null;
1718
+ config;
1719
+ api_prefix = "";
1720
+ api_info;
1721
+ api_map = {};
1722
+ session_hash = Math.random().toString(36).substring(2);
1723
+ jwt = !1;
1724
+ last_status = {};
1725
+ cookies = null;
1726
+ // streaming
1727
+ stream_status = { open: !1 };
1728
+ closed = !1;
1729
+ pending_stream_messages = {};
1730
+ pending_diff_streams = {};
1731
+ event_callbacks = {};
1732
+ unclosed_events = /* @__PURE__ */ new Set();
1733
+ heartbeat_event = null;
1734
+ abort_controller = null;
1735
+ stream_instance = null;
1736
+ current_payload;
1737
+ get_url_config(s = null) {
1738
+ if (!this.config)
1739
+ throw new Error(L);
1740
+ s === null && (s = window.location.href);
1741
+ const t = (i) => i.replace(/^\/+|\/+$/g, "");
1742
+ let n = t(new URL(this.config.root).pathname), o = t(new URL(s).pathname), r;
1743
+ return o.startsWith(n) ? r = t(o.substring(n.length)) : r = "", this.get_page_config(r);
1744
+ }
1745
+ get_page_config(s) {
1746
+ if (!this.config)
1747
+ throw new Error(L);
1748
+ let t = this.config;
1749
+ return s in t.page || (s = ""), {
1750
+ ...t,
1751
+ current_page: s,
1752
+ layout: t.page[s].layout,
1753
+ components: t.components.filter(
1754
+ (n) => t.page[s].components.includes(n.id)
1755
+ ),
1756
+ dependencies: this.config.dependencies.filter(
1757
+ (n) => t.page[s].dependencies.includes(n.id)
1758
+ )
1759
+ };
1760
+ }
1761
+ fetch(s, t) {
1762
+ const n = new Headers(t?.headers || {});
1763
+ return this && this.cookies && n.append("Cookie", this.cookies), this && this.options.headers && new Headers(this.options.headers).forEach((r, i) => {
1764
+ n.append(i, r);
1765
+ }), fetch(s, { ...t, headers: n });
1766
+ }
1767
+ stream(s) {
1768
+ const t = new Headers();
1769
+ return this && this.cookies && t.append("Cookie", this.cookies), this && this.options.headers && new Headers(this.options.headers).forEach((o, r) => {
1770
+ t.append(r, o);
1771
+ }), this && this.options.token && t.append("Authorization", `Bearer ${this.options.token}`), this.abort_controller = new AbortController(), this.stream_instance = Qt(s.toString(), {
1772
+ credentials: this.options.credentials ?? "same-origin",
1773
+ headers: t,
1774
+ signal: this.abort_controller.signal
1775
+ }), this.stream_instance;
1776
+ }
1777
+ view_api;
1778
+ upload_files;
1779
+ upload;
1780
+ handle_blob;
1781
+ post_data;
1782
+ submit;
1783
+ predict;
1784
+ open_stream;
1785
+ resolve_config;
1786
+ resolve_cookies;
1787
+ constructor(s, t = { events: ["data"] }) {
1788
+ this.app_reference = s, this.deep_link = t.query_params?.deep_link || null, t.events || (t.events = ["data"]), this.options = t, this.current_payload = {}, t.cookies && (this.cookies = t.cookies), this.view_api = vt.bind(this), this.upload_files = Et.bind(this), this.handle_blob = Dt.bind(this), this.post_data = Ct.bind(this), this.submit = Xt.bind(this), this.predict = Pt.bind(this), this.open_stream = Zt.bind(this), this.resolve_config = ft.bind(this), this.resolve_cookies = _t.bind(this), this.upload = Ot.bind(this), this.fetch = this.fetch.bind(this), this.handle_space_success = this.handle_space_success.bind(this), this.stream = this.stream.bind(this);
1789
+ }
1790
+ async init() {
1791
+ Gt(), this.options.auth && await this.resolve_cookies(), await this._resolve_config().then(
1792
+ ({ config: s }) => this._resolve_heartbeat(s)
1793
+ ), this.api_info = await this.view_api(), this.api_map = ht(this.config?.dependencies || []);
1794
+ }
1795
+ async _resolve_heartbeat(s) {
1796
+ if (s && (this.config = s, this.api_prefix = s.api_prefix || "", this.config && this.config.connect_heartbeat && this.config.space_id && this.options.token && (this.jwt = await be(
1797
+ this.config.space_id,
1798
+ this.options.token,
1799
+ this.cookies
1800
+ ))), s.space_id && this.options.token && (this.jwt = await be(s.space_id, this.options.token)), this.config && this.config.connect_heartbeat) {
1801
+ const t = new URL(
1802
+ `${this.config.root}${this.api_prefix}/${tt}/${this.session_hash}`
1803
+ );
1804
+ this.jwt && t.searchParams.set("__sign", this.jwt), this.heartbeat_event || (this.heartbeat_event = this.stream(t));
1805
+ }
1806
+ }
1807
+ static async connect(s, t = {
1808
+ events: ["data"]
1809
+ }) {
1810
+ const n = new this(s, t);
1811
+ return t.session_hash && (n.session_hash = t.session_hash), await n.init(), n;
1812
+ }
1813
+ async reconnect() {
1814
+ const s = new URL(
1815
+ `${this.config.root}${this.api_prefix}/${ot}`
1816
+ );
1817
+ let t;
1818
+ try {
1819
+ const n = await this.fetch(s);
1820
+ if (!n.ok)
1821
+ throw new Error();
1822
+ t = (await n.json()).app_id;
1823
+ } catch {
1824
+ return "broken";
1825
+ }
1826
+ return t !== this.config.app_id ? "changed" : "connected";
1827
+ }
1828
+ close() {
1829
+ this.closed = !0, de(this.stream_status, this.abort_controller);
1830
+ }
1831
+ set_current_payload(s) {
1832
+ this.current_payload = s;
1833
+ }
1834
+ static async duplicate(s, t = {
1835
+ events: ["data"]
1836
+ }) {
1837
+ return qt(s, t);
1838
+ }
1839
+ async _resolve_config() {
1840
+ const { http_protocol: s, host: t, space_id: n } = await pe(
1841
+ this.app_reference,
1842
+ this.options.token
1843
+ ), { status_callback: o } = this.options;
1844
+ n && o && await je(n, o);
1845
+ let r;
1846
+ try {
1847
+ let i = `${s}//${t}`;
1848
+ if (r = await this.resolve_config(i), !r)
1849
+ throw new Error(L);
1850
+ return this.config_success(r);
1851
+ } catch (i) {
1852
+ if (n && o)
1853
+ F(
1854
+ n,
1855
+ le.test(n) ? "space_name" : "subdomain",
1856
+ this.handle_space_success
1857
+ );
1858
+ else
1859
+ throw o && o({
1860
+ status: "error",
1861
+ message: "Could not load this space.",
1862
+ load_status: "error",
1863
+ detail: "NOT_FOUND"
1864
+ }), Error(i);
1865
+ }
1866
+ }
1867
+ async config_success(s) {
1868
+ if (this.config = s, this.api_prefix = s.api_prefix || "", this.config.auth_required)
1869
+ return this.prepare_return_obj();
1870
+ try {
1871
+ this.api_info = await this.view_api();
1872
+ } catch (t) {
1873
+ console.error(at + t.message);
1874
+ }
1875
+ return this.prepare_return_obj();
1876
+ }
1877
+ async handle_space_success(s) {
1878
+ if (!this)
1879
+ throw new Error(L);
1880
+ const { status_callback: t } = this.options;
1881
+ if (t && t(s), s.status === "running")
1882
+ try {
1883
+ if (this.config = await this._resolve_config(), this.api_prefix = this?.config?.api_prefix || "", !this.config)
1884
+ throw new Error(L);
1885
+ return await this.config_success(this.config);
1886
+ } catch (n) {
1887
+ throw t && t({
1888
+ status: "error",
1889
+ message: "Could not load this space.",
1890
+ load_status: "error",
1891
+ detail: "NOT_FOUND"
1892
+ }), n;
1893
+ }
1894
+ }
1895
+ async component_server(s, t, n) {
1896
+ if (!this.config)
1897
+ throw new Error(L);
1898
+ const o = {}, { token: r } = this.options, { session_hash: i } = this;
1899
+ r && (o.Authorization = `Bearer ${this.options.token}`);
1900
+ let a, u = this.config.components.find(
1901
+ (p) => p.id === s
1902
+ );
1903
+ u?.props?.root_url ? a = u.props.root_url : a = this.config.root;
1904
+ let l;
1905
+ if (typeof n == "object" && n !== null && "binary" in n) {
1906
+ const p = n;
1907
+ l = new FormData();
1908
+ for (const c in p.data)
1909
+ c !== "binary" && l.append(c, p.data[c]);
1910
+ l.set("component_id", s.toString()), l.set("fn_name", t), l.set("session_hash", i);
1911
+ } else
1912
+ l = JSON.stringify({
1913
+ data: n,
1914
+ component_id: s,
1915
+ fn_name: t,
1916
+ session_hash: i
1917
+ }), o["Content-Type"] = "application/json";
1918
+ r && (o.Authorization = `Bearer ${r}`);
1919
+ try {
1920
+ const p = await this.fetch(
1921
+ `${a}${this.api_prefix}/${st}/`,
1922
+ {
1923
+ method: "POST",
1924
+ body: l,
1925
+ headers: o,
1926
+ credentials: this.options.credentials ?? "same-origin"
1927
+ }
1928
+ );
1929
+ if (!p.ok)
1930
+ throw new Error(
1931
+ "Could not connect to component server: " + p.statusText
1932
+ );
1933
+ return await p.json();
1934
+ } catch (p) {
1935
+ console.warn(p);
1936
+ }
1937
+ }
1938
+ set_cookies(s) {
1939
+ this.cookies = Pe(s).join("; ");
1940
+ }
1941
+ prepare_return_obj() {
1942
+ return {
1943
+ config: this.config,
1944
+ predict: this.predict,
1945
+ submit: this.submit,
1946
+ view_api: this.view_api,
1947
+ component_server: this.component_server
1948
+ };
1949
+ }
1950
+ }
1951
+ async function os(e, s = {
1952
+ events: ["data"]
1953
+ }) {
1954
+ return await Y.connect(e, s);
1955
+ }
1956
+ async function rs(e, s) {
1957
+ return await Y.duplicate(e, s);
1958
+ }
1959
+ export {
1960
+ Y as Client,
1961
+ Q as FileData,
1962
+ lt as MISSING_CREDENTIALS_MSG,
1963
+ os as client,
1964
+ rs as duplicate,
1965
+ is as handle_file,
1966
+ Pt as predict,
1967
+ ns as prepare_files,
1968
+ Xt as submit,
1969
+ Ot as upload,
1970
+ Et as upload_files
1971
+ };
index.html CHANGED
@@ -1,47 +1,716 @@
1
- <!doctype html>
2
  <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>hy3dlab Image-to-Image API</title>
7
- <style>
8
- :root {
9
- color-scheme: dark;
10
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
11
- background: #0b0b0f;
12
- color: #f3f4f6;
13
- }
14
- body {
15
- margin: 0;
16
- min-height: 100vh;
17
- display: grid;
18
- place-items: center;
19
- }
20
- main {
21
- width: min(42rem, calc(100% - 3rem));
22
- padding: 2rem;
23
- border: 1px solid #2d2d36;
24
- border-radius: 1rem;
25
- background: #141419;
26
- }
27
- h1 {
28
- margin-top: 0;
29
- font-size: 1.5rem;
30
- }
31
- p {
32
- line-height: 1.65;
33
- color: #c7c9d1;
34
- }
35
- code {
36
- color: #d8b4fe;
37
- }
38
- </style>
39
- </head>
40
- <body>
41
- <main>
42
- <h1>hy3dlab Image-to-Image API</h1>
43
- <p>This public Space provides the backend API for the first-party hy3d.dev UI.</p>
44
- <p>The named Gradio endpoint is <code>/edit_image</code>. No task is submitted by loading this page.</p>
45
- </main>
46
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  </html>
 
1
+ <!DOCTYPE html>
2
  <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Hy3D Lab · AI Image Editor</title>
7
+ <style>
8
+ *{box-sizing:border-box;margin:0;padding:0}
9
+ :root{
10
+ --bg:#0a0a0c;--panel:#131316;--panel2:#0e0e11;--canvas:#08080a;
11
+ --border:#232328;--border2:#33333a;
12
+ --text:#e4e4e7;--muted:#a1a1aa;--dim:#6b6b75;--faint:#4a4a52;
13
+ --accent:#1E90FF;--accent2:#47A3FF;--accent3:#7CB8FF;
14
+ --menubar-h:48px;--statusbar-h:30px;--rail-w:52px;--inspector-w:360px;--filmstrip-h:132px;
15
+ }
16
+ html,body{height:100%}
17
+ body{
18
+ background:var(--bg);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
19
+ font-size:13px;color:var(--text);overflow:hidden;
20
+ }
21
+
22
+ /* ══ Layout grid ══ */
23
+ .studio{
24
+ display:grid;height:100vh;
25
+ grid-template-rows:var(--menubar-h) 1fr var(--statusbar-h);
26
+ grid-template-columns:var(--rail-w) 1fr var(--inspector-w);
27
+ grid-template-areas:
28
+ "menubar menubar menubar"
29
+ "rail workspace inspector"
30
+ "statusbar statusbar statusbar";
31
+ }
32
+
33
+ /* ══ Menu bar ══ */
34
+ .menubar{
35
+ grid-area:menubar;display:flex;align-items:center;gap:14px;
36
+ background:var(--panel);border-bottom:1px solid var(--border);
37
+ padding:0 16px;z-index:50;
38
+ }
39
+ .menubar-logo{
40
+ width:28px;height:28px;background:linear-gradient(135deg,#1E90FF,#47A3FF,#7CB8FF);
41
+ border-radius:8px;display:flex;align-items:center;justify-content:center;
42
+ box-shadow:0 2px 8px rgba(30,144,255,.35);flex-shrink:0;
43
+ }
44
+ .menubar-logo svg{width:15px;height:15px;fill:#fff}
45
+ .menubar-title{font-size:14px;font-weight:700;letter-spacing:-.2px;white-space:nowrap}
46
+ .menubar-title span{color:var(--dim);font-weight:500}
47
+ .menubar-badge{
48
+ font-size:10px;font-weight:700;padding:2px 8px;border-radius:12px;
49
+ background:rgba(30,144,255,.14);color:var(--accent2);border:1px solid rgba(30,144,255,.25);
50
+ letter-spacing:.4px;white-space:nowrap;
51
+ }
52
+ .menubar-badge.fast{background:rgba(34,197,94,.12);color:#4ade80;border-color:rgba(34,197,94,.25)}
53
+ .menubar-spacer{flex:1}
54
+ .menubar-status{
55
+ display:flex;align-items:center;gap:7px;font-family:'JetBrains Mono',monospace;
56
+ font-size:11px;color:var(--dim);
57
+ }
58
+ .menubar-status .dot{width:7px;height:7px;border-radius:50%;background:#4ade80;box-shadow:0 0 6px rgba(74,222,128,.6)}
59
+ .menubar-status .dot.pending{background:#facc15;box-shadow:0 0 6px rgba(250,204,21,.45)}
60
+ .menubar-status .dot.off{background:#f87171;box-shadow:0 0 6px rgba(248,113,113,.5)}
61
+ .site-btn{
62
+ display:inline-flex;align-items:center;gap:6px;
63
+ padding:5px 13px;border-radius:7px;text-decoration:none;
64
+ font-size:12px;font-weight:600;background:var(--accent);color:#fff;
65
+ border:1px solid rgba(255,255,255,.15);
66
+ transition:all .15s ease;flex-shrink:0;
67
+ }
68
+ .site-btn:hover{background:var(--accent2)}
69
+ .site-btn svg{fill:none;stroke:currentColor;stroke-width:2;width:13px;height:13px}
70
+
71
+ /* ══ Left icon rail ══ */
72
+ .rail{
73
+ grid-area:rail;background:var(--panel);border-right:1px solid var(--border);
74
+ display:flex;flex-direction:column;align-items:center;padding:10px 0;gap:4px;
75
+ overflow-y:auto;z-index:40;
76
+ }
77
+ .rail-btn{
78
+ width:38px;height:38px;border-radius:9px;border:1px solid transparent;
79
+ background:transparent;color:var(--dim);cursor:pointer;
80
+ display:flex;align-items:center;justify-content:center;
81
+ transition:all .15s ease;position:relative;
82
+ }
83
+ .rail-btn:hover:not(:disabled){background:rgba(30,144,255,.12);color:var(--accent3);border-color:rgba(30,144,255,.25)}
84
+ .rail-btn.active{background:rgba(30,144,255,.2);color:var(--accent2);border-color:rgba(30,144,255,.4)}
85
+ .rail-btn:disabled{opacity:.3;cursor:not-allowed}
86
+ .rail-btn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
87
+ .rail-btn svg.fill{fill:currentColor;stroke:none}
88
+ .rail-sep{width:24px;height:1px;background:var(--border);margin:6px 0}
89
+ .rail-label{
90
+ font-size:8px;font-weight:700;color:var(--faint);letter-spacing:1px;
91
+ text-transform:uppercase;margin:8px 0 2px;user-select:none;
92
+ }
93
+
94
+ /* ══ Workspace (canvas + filmstrip) ══ */
95
+ .workspace{
96
+ grid-area:workspace;display:flex;flex-direction:column;min-width:0;min-height:0;
97
+ background:var(--canvas);
98
+ }
99
+ .canvas-head{
100
+ display:flex;align-items:center;gap:10px;padding:8px 16px;
101
+ border-bottom:1px solid var(--border);background:var(--panel2);flex-shrink:0;
102
+ }
103
+ .seg-control{
104
+ display:inline-flex;background:var(--panel);border:1px solid var(--border);
105
+ border-radius:8px;padding:2px;gap:2px;
106
+ }
107
+ .seg-btn{
108
+ padding:4px 14px;font-size:11px;font-weight:600;color:var(--dim);
109
+ background:transparent;border:none;border-radius:6px;cursor:pointer;
110
+ font-family:'Inter',sans-serif;transition:all .15s;letter-spacing:.2px;
111
+ }
112
+ .seg-btn:hover:not(.active){color:var(--muted)}
113
+ .seg-btn.active{background:rgba(30,144,255,.18);color:var(--accent2)}
114
+ .canvas-meta{
115
+ font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--faint);
116
+ overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex:1;
117
+ }
118
+ .canvas-actions{display:flex;align-items:center;gap:6px}
119
+ .canvas-action-btn{
120
+ display:none;align-items:center;gap:5px;background:rgba(30,144,255,.1);
121
+ border:1px solid rgba(30,144,255,.2);border-radius:6px;cursor:pointer;padding:4px 11px;
122
+ font-size:11px;font-weight:500;color:var(--accent3);height:26px;transition:all .15s;
123
+ font-family:'Inter',sans-serif;white-space:nowrap;
124
+ }
125
+ .canvas-action-btn:hover{background:rgba(30,144,255,.2);border-color:rgba(30,144,255,.35);color:#fff}
126
+ .canvas-action-btn.visible{display:inline-flex}
127
+ .canvas-action-btn.active{background:rgba(30,144,255,.3);border-color:var(--accent);color:#fff}
128
+ .canvas-action-btn svg{width:12px;height:12px;fill:currentColor}
129
+ .out-seed-chip{
130
+ display:none;font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:500;
131
+ color:var(--dim);background:var(--panel);border:1px solid var(--border);
132
+ border-radius:6px;padding:4px 9px;cursor:pointer;transition:all .15s;
133
+ }
134
+ .out-seed-chip.visible{display:inline-flex}
135
+ .out-seed-chip:hover{color:var(--accent3);border-color:rgba(30,144,255,.35)}
136
+
137
+ .canvas-stage{
138
+ flex:1;position:relative;overflow:hidden;min-height:0;
139
+ display:flex;align-items:center;justify-content:center;
140
+ background:
141
+ repeating-conic-gradient(#0c0c0f 0% 25%, #08080a 0% 50%) 0 0/24px 24px;
142
+ }
143
+ .canvas-stage img.modern-out-img{
144
+ max-width:calc(100% - 48px);max-height:calc(100% - 48px);
145
+ box-shadow:0 12px 48px rgba(0,0,0,.7),0 0 0 1px var(--border);
146
+ border-radius:4px;cursor:zoom-in;image-rendering:auto;
147
+ animation:canvasIn .3s ease;
148
+ }
149
+ @keyframes canvasIn{from{opacity:0;transform:scale(.985)}to{opacity:1;transform:scale(1)}}
150
+ .canvas-placeholder{
151
+ text-align:center;color:var(--faint);user-select:none;
152
+ display:flex;flex-direction:column;align-items:center;gap:14px;padding:24px;
153
+ }
154
+ .canvas-placeholder svg{width:72px;height:72px;opacity:.5}
155
+ .canvas-placeholder .cp-title{font-size:15px;font-weight:600;color:var(--dim)}
156
+ .canvas-placeholder .cp-sub{font-size:12px;line-height:1.7;max-width:340px}
157
+ .canvas-placeholder kbd{
158
+ padding:1px 6px;background:var(--panel);border:1px solid var(--border2);
159
+ border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--muted);
160
+ }
161
+
162
+ /* Compare slider */
163
+ .compare-wrap{
164
+ position:relative;max-width:calc(100% - 48px);max-height:calc(100% - 48px);overflow:hidden;
165
+ display:none;user-select:none;touch-action:none;cursor:ew-resize;
166
+ box-shadow:0 12px 48px rgba(0,0,0,.7),0 0 0 1px var(--border);border-radius:4px;
167
+ }
168
+ .compare-wrap.visible{display:block}
169
+ .compare-wrap:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
170
+ .compare-wrap img{display:block;max-width:100%;max-height:100%;pointer-events:none}
171
+ .compare-top{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden}
172
+ .compare-top img{position:absolute;top:0;left:0;height:100%;width:auto;max-width:none!important;max-height:none!important}
173
+ .compare-divider{position:absolute;top:0;bottom:0;width:2px;background:#fff;box-shadow:0 0 8px rgba(0,0,0,.6);pointer-events:none}
174
+ .compare-handle{
175
+ position:absolute;top:50%;transform:translate(-50%,-50%);
176
+ width:32px;height:32px;border-radius:50%;background:#fff;
177
+ display:flex;align-items:center;justify-content:center;
178
+ box-shadow:0 2px 8px rgba(0,0,0,.5);color:#111;font-size:13px;font-weight:800;
179
+ }
180
+ .compare-label{
181
+ position:absolute;top:8px;padding:2px 8px;border-radius:4px;
182
+ background:rgba(0,0,0,.65);color:#fff;font-size:10px;font-weight:600;
183
+ font-family:'JetBrains Mono',monospace;pointer-events:none;
184
+ }
185
+ .compare-label.before{left:8px}
186
+ .compare-label.after{right:8px}
187
+
188
+ /* Loader */
189
+ .modern-loader{
190
+ display:none;position:absolute;inset:0;background:rgba(8,8,10,.9);
191
+ z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(5px);
192
+ }
193
+ .modern-loader.active{display:flex}
194
+ .loader-spinner{
195
+ width:38px;height:38px;border:3px solid var(--border);border-top-color:var(--accent);
196
+ border-radius:50%;animation:spin .8s linear infinite;
197
+ }
198
+ @keyframes spin{to{transform:rotate(360deg)}}
199
+ .loader-text{font-size:13px;color:var(--muted);font-weight:500;text-align:center;padding:0 16px}
200
+ .loader-bar-track{width:220px;height:4px;background:var(--border);border-radius:2px;overflow:hidden}
201
+ .loader-bar-fill{
202
+ height:100%;width:100%;background:linear-gradient(90deg,#1E90FF,#47A3FF,#1E90FF);
203
+ background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
204
+ transition:width .3s ease;
205
+ }
206
+ .loader-bar-fill.determinate{animation:none;background:var(--accent)}
207
+ @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
208
+
209
+ /* ══ Filmstrip ══ */
210
+ .filmstrip{
211
+ height:var(--filmstrip-h);flex-shrink:0;background:var(--panel);
212
+ border-top:1px solid var(--border);display:flex;min-height:0;overflow:hidden;
213
+ }
214
+ .filmstrip-section{
215
+ display:flex;flex-direction:column;min-width:0;padding:8px 0 8px 14px;
216
+ }
217
+ .filmstrip-section.inputs{flex:1.2;border-right:1px solid var(--border)}
218
+ .filmstrip-section.history{flex:1}
219
+ .filmstrip-label{
220
+ font-size:9px;font-weight:700;color:var(--faint);letter-spacing:1.2px;
221
+ text-transform:uppercase;margin-bottom:6px;display:flex;align-items:center;gap:8px;
222
+ user-select:none;flex-shrink:0;
223
+ }
224
+ .filmstrip-label .count{
225
+ font-family:'JetBrains Mono',monospace;font-weight:500;color:var(--dim);
226
+ background:var(--panel2);border:1px solid var(--border);border-radius:4px;padding:0 5px;font-size:9px;
227
+ }
228
+ .filmstrip-row{
229
+ display:flex;gap:8px;overflow-x:auto;overflow-y:hidden;flex:1;
230
+ align-items:flex-start;padding-bottom:4px;padding-right:14px;
231
+ }
232
+ .filmstrip-row::-webkit-scrollbar{height:5px}
233
+ .filmstrip-row::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
234
+ .fs-thumb{
235
+ position:relative;flex-shrink:0;width:76px;height:76px;border-radius:8px;overflow:hidden;
236
+ border:2px solid var(--border);cursor:pointer;transition:all .15s;background:var(--panel2);
237
+ animation:thumbIn .25s ease;
238
+ }
239
+ @keyframes thumbIn{from{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}
240
+ .fs-thumb:hover{border-color:var(--border2);transform:translateY(-2px)}
241
+ .fs-thumb.selected{border-color:var(--accent);box-shadow:0 0 0 2px rgba(30,144,255,.25)}
242
+ .fs-thumb img{width:100%;height:100%;object-fit:cover}
243
+ .fs-badge{
244
+ position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.75);color:#fff;
245
+ padding:1px 6px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:9px;font-weight:600;
246
+ }
247
+ .fs-remove{
248
+ position:absolute;top:4px;right:4px;width:20px;height:20px;background:rgba(0,0,0,.8);
249
+ color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:50%;cursor:pointer;
250
+ display:none;align-items:center;justify-content:center;font-size:10px;transition:all .15s;line-height:1;
251
+ }
252
+ .fs-thumb:hover .fs-remove{display:flex}
253
+ .fs-remove:hover{background:#ef4444;border-color:#ef4444}
254
+ .fs-add{
255
+ flex-shrink:0;width:76px;height:76px;border-radius:8px;border:2px dashed var(--border2);
256
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
257
+ cursor:pointer;transition:all .2s;background:rgba(30,144,255,.03);gap:3px;
258
+ }
259
+ .fs-add:hover{border-color:var(--accent);background:rgba(30,144,255,.08)}
260
+ .fs-add .add-icon{font-size:22px;color:var(--dim);font-weight:300}
261
+ .fs-add .add-text{font-size:9px;color:var(--dim);font-weight:600;text-transform:uppercase;letter-spacing:.5px}
262
+ .fs-empty{
263
+ flex-shrink:0;display:flex;align-items:center;height:76px;
264
+ color:var(--faint);font-size:11px;font-style:italic;padding-right:12px;
265
+ }
266
+
267
+ /* ══ Inspector (right panel) ══ */
268
+ .inspector{
269
+ grid-area:inspector;background:var(--panel);border-left:1px solid var(--border);
270
+ display:flex;flex-direction:column;min-height:0;z-index:40;
271
+ }
272
+ .inspector-tabs{
273
+ display:flex;border-bottom:1px solid var(--border);flex-shrink:0;
274
+ }
275
+ .insp-tab{
276
+ flex:1;padding:11px 8px;font-size:11px;font-weight:700;color:var(--dim);
277
+ background:transparent;border:none;border-bottom:2px solid transparent;
278
+ cursor:pointer;font-family:'Inter',sans-serif;text-transform:uppercase;letter-spacing:.8px;
279
+ transition:all .15s;
280
+ }
281
+ .insp-tab:hover:not(.active){color:var(--muted)}
282
+ .insp-tab.active{color:var(--accent2);border-bottom-color:var(--accent)}
283
+ .insp-page{display:none;flex:1;overflow-y:auto;min-height:0;flex-direction:column}
284
+ .insp-page.active{display:flex}
285
+
286
+ .insp-section{border-bottom:1px solid var(--border);padding:14px 18px}
287
+ .insp-section-title{
288
+ font-size:10px;font-weight:700;color:var(--dim);text-transform:uppercase;
289
+ letter-spacing:1px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between;
290
+ }
291
+ .char-count{font-size:9px;font-weight:500;color:var(--faint);font-family:'JetBrains Mono',monospace;text-transform:none;letter-spacing:0}
292
+ .modern-textarea{
293
+ width:100%;background:var(--panel2);border:1px solid var(--border);border-radius:8px;
294
+ padding:10px 13px;font-family:'Inter',sans-serif;font-size:13px;color:var(--text);
295
+ resize:vertical;outline:none;min-height:64px;transition:border-color .2s;line-height:1.55;
296
+ }
297
+ .modern-textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(30,144,255,.15)}
298
+ .modern-textarea::placeholder{color:var(--faint)}
299
+ .modern-textarea.error-flash{
300
+ border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease;
301
+ }
302
+ @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
303
+
304
+ .suggestions-wrap{display:flex;flex-wrap:wrap;gap:5px}
305
+ .suggestion-chip{
306
+ display:inline-flex;align-items:center;padding:4px 11px;
307
+ background:rgba(30,144,255,.07);border:1px solid rgba(30,144,255,.18);border-radius:14px;
308
+ color:var(--accent3);font-size:11px;font-weight:500;font-family:'Inter',sans-serif;
309
+ cursor:pointer;transition:all .15s;white-space:nowrap;
310
+ }
311
+ .suggestion-chip:hover{background:rgba(30,144,255,.15);border-color:rgba(30,144,255,.35);color:var(--accent2)}
312
+ .suggestion-chip.active{background:rgba(30,144,255,.25);border-color:var(--accent);color:#fff}
313
+
314
+ .btn-run{
315
+ display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
316
+ background:linear-gradient(135deg,#1E90FF,#1873CC);border:none;border-radius:9px;
317
+ padding:12px 24px;cursor:pointer;font-size:14px;font-weight:700;font-family:'Inter',sans-serif;
318
+ color:#fff;transition:all .2s ease;letter-spacing:-.2px;
319
+ box-shadow:0 4px 16px rgba(30,144,255,.3),inset 0 1px 0 rgba(255,255,255,.1);
320
+ }
321
+ .btn-run:hover:not(:disabled){
322
+ background:linear-gradient(135deg,#47A3FF,#1E90FF);transform:translateY(-1px);
323
+ box-shadow:0 6px 24px rgba(30,144,255,.45),inset 0 1px 0 rgba(255,255,255,.15);
324
+ }
325
+ .btn-run:active:not(:disabled){transform:translateY(0)}
326
+ .btn-run:disabled{opacity:.55;cursor:not-allowed}
327
+ .btn-run svg{width:16px;height:16px;fill:#fff}
328
+ .btn-cancel{
329
+ display:flex;align-items:center;justify-content:center;gap:6px;width:100%;
330
+ margin-top:8px;background:transparent;border:1px solid var(--border2);border-radius:9px;
331
+ padding:8px 24px;cursor:pointer;font-size:12px;font-weight:600;font-family:'Inter',sans-serif;
332
+ color:var(--muted);transition:all .15s;
333
+ }
334
+ .btn-cancel:hover{border-color:#ef4444;color:#ef4444;background:rgba(239,68,68,.08)}
335
+ .run-hint{
336
+ text-align:center;font-size:10px;color:var(--faint);margin-top:8px;
337
+ font-family:'JetBrains Mono',monospace;
338
+ }
339
+
340
+ .slider-row{display:flex;align-items:center;gap:9px;min-height:26px;margin-bottom:10px}
341
+ .slider-row:last-child{margin-bottom:0}
342
+ .slider-row label{font-size:12px;font-weight:500;color:var(--muted);min-width:64px;flex-shrink:0}
343
+ .slider-row input[type="range"]{
344
+ flex:1;-webkit-appearance:none;appearance:none;height:5px;background:var(--border);
345
+ border-radius:3px;outline:none;min-width:0;
346
+ }
347
+ .slider-row input[type="range"]::-webkit-slider-thumb{
348
+ -webkit-appearance:none;width:14px;height:14px;background:linear-gradient(135deg,#1E90FF,#1873CC);
349
+ border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(30,144,255,.4);transition:transform .15s;
350
+ }
351
+ .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}
352
+ .slider-row input[type="range"]::-moz-range-thumb{
353
+ width:14px;height:14px;background:linear-gradient(135deg,#1E90FF,#1873CC);
354
+ border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(30,144,255,.4);
355
+ }
356
+ .slider-val{
357
+ min-width:48px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:11px;
358
+ font-weight:500;padding:2px 7px;background:var(--panel2);border:1px solid var(--border);
359
+ border-radius:5px;color:var(--muted);flex-shrink:0;
360
+ }
361
+ .dice-btn{
362
+ display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;
363
+ background:var(--panel2);border:1px solid var(--border);border-radius:5px;cursor:pointer;
364
+ color:var(--dim);transition:all .15s;flex-shrink:0;padding:0;
365
+ }
366
+ .dice-btn:hover{border-color:var(--accent);color:var(--accent3)}
367
+ .dice-btn:disabled{opacity:.35;cursor:not-allowed;border-color:var(--border);color:var(--dim)}
368
+ .dice-btn svg{width:13px;height:13px;fill:currentColor}
369
+ .checkbox-row{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted);margin-bottom:10px}
370
+ .checkbox-row input[type="checkbox"]{accent-color:var(--accent);width:15px;height:15px;cursor:pointer}
371
+ .checkbox-row label{color:var(--muted);font-size:12px;cursor:pointer}
372
+ .advanced-section{border-bottom:1px solid var(--border)}
373
+ .advanced-section summary{
374
+ padding:14px 18px;cursor:pointer;list-style:none;font-size:10px;font-weight:700;
375
+ color:var(--dim);text-transform:uppercase;letter-spacing:1px;user-select:none;
376
+ display:flex;align-items:center;justify-content:space-between;
377
+ }
378
+ .advanced-section summary::-webkit-details-marker{display:none}
379
+ .advanced-section summary::after{content:'›';font-size:18px;line-height:1;transition:transform .15s}
380
+ .advanced-section[open] summary::after{transform:rotate(90deg)}
381
+ .advanced-content{padding:0 18px 14px}
382
+
383
+ /* ══ Status bar ══ */
384
+ .statusbar{
385
+ grid-area:statusbar;background:var(--panel);border-top:1px solid var(--border);
386
+ display:flex;align-items:center;gap:4px;padding:0 14px;font-size:11px;z-index:50;
387
+ }
388
+ .statusbar .sb-section{
389
+ padding:0 10px;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;
390
+ font-size:11px;color:var(--faint);overflow:hidden;white-space:nowrap;gap:6px;
391
+ }
392
+ .statusbar .sb-right{margin-left:auto;display:flex;align-items:center;gap:10px}
393
+ .sb-pill{
394
+ padding:2px 12px;border-radius:5px;font-weight:600;font-size:10px;
395
+ background:rgba(30,144,255,.1);color:var(--accent2);
396
+ }
397
+ .sb-pill.error{background:rgba(239,68,68,.12);color:#f87171}
398
+ .sb-pill.done{background:rgba(34,197,94,.12);color:#4ade80}
399
+ .sb-link{color:var(--faint);text-decoration:none;font-family:'JetBrains Mono',monospace;font-size:10px}
400
+ .sb-link:hover{color:var(--accent3)}
401
+
402
+ /* ══ Toast ══ */
403
+ .toast-notification{
404
+ position:fixed;top:60px;left:50%;transform:translateX(-50%) translateY(-140%);
405
+ z-index:9999;padding:10px 22px;border-radius:9px;font-family:'Inter',sans-serif;
406
+ font-size:13px;font-weight:600;display:flex;align-items:center;gap:8px;
407
+ box-shadow:0 8px 24px rgba(0,0,0,.5);
408
+ transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none;
409
+ }
410
+ .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
411
+ .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}
412
+ .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)}
413
+ .toast-notification.info{background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border:1px solid rgba(255,255,255,.15)}
414
+ .toast-notification .toast-icon{font-size:15px;line-height:1}
415
+
416
+ /* ══ Lightbox ══ */
417
+ .lightbox{
418
+ position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:10000;
419
+ display:none;align-items:center;justify-content:center;cursor:zoom-out;
420
+ backdrop-filter:blur(6px);animation:fadeIn .2s ease;
421
+ }
422
+ .lightbox.visible{display:flex}
423
+ .lightbox img{max-width:94vw;max-height:94vh;border-radius:6px;box-shadow:0 20px 60px rgba(0,0,0,.8)}
424
+ .lightbox-close{
425
+ position:fixed;top:18px;right:18px;width:42px;height:42px;border-radius:50%;
426
+ border:1px solid var(--border2);background:rgba(19,19,22,.92);color:var(--text);
427
+ font-size:24px;line-height:1;cursor:pointer;
428
+ }
429
+ .lightbox-close:hover{border-color:var(--accent);color:var(--accent3)}
430
+ @keyframes fadeIn{from{opacity:0}to{opacity:1}}
431
+
432
+ /* ══ Drop overlay ══ */
433
+ .drop-overlay{
434
+ position:fixed;inset:0;z-index:9000;background:rgba(8,8,10,.85);
435
+ display:none;align-items:center;justify-content:center;pointer-events:none;
436
+ backdrop-filter:blur(3px);
437
+ }
438
+ .drop-overlay.visible{display:flex}
439
+ .drop-overlay-inner{
440
+ border:3px dashed var(--accent);border-radius:22px;padding:56px 76px;
441
+ font-size:19px;font-weight:700;color:var(--accent2);background:rgba(30,144,255,.06);
442
+ }
443
+
444
+ /* ══ Scrollbars ══ */
445
+ ::-webkit-scrollbar{width:8px;height:8px}
446
+ ::-webkit-scrollbar-track{background:transparent}
447
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
448
+ ::-webkit-scrollbar-thumb:hover{background:var(--border2)}
449
+
450
+ /* ══ Touch devices: hover-only controls must always be visible ══ */
451
+ @media (pointer:coarse){
452
+ .fs-remove{display:flex}
453
+ .rail-btn{width:44px;height:44px}
454
+ .rail-btn svg{width:19px;height:19px}
455
+ }
456
+
457
+ /* ══ Responsive: tablet & phone ══ */
458
+ @media(max-width:960px){
459
+ html,body{height:auto}
460
+ body{overflow:auto!important}
461
+ .studio{
462
+ display:flex;flex-direction:column;height:auto;min-height:100vh;
463
+ }
464
+
465
+ /* Menu bar */
466
+ .menubar{
467
+ flex-wrap:wrap;gap:8px;padding:10px 12px;min-height:var(--menubar-h);
468
+ position:sticky;top:0;order:1;
469
+ }
470
+ .menubar-title{font-size:13px}
471
+ .menubar-status{display:flex;order:6;flex-basis:100%;font-size:10px}
472
+ .menubar-spacer{flex:1}
473
+ .site-btn{padding:6px 10px}
474
+ .site-btn span{display:none}
475
+
476
+ /* Rail → horizontal tool strip */
477
+ .rail{
478
+ flex-direction:row;justify-content:flex-start;flex-wrap:nowrap;
479
+ overflow-x:auto;overflow-y:hidden;border-right:none;
480
+ border-bottom:1px solid var(--border);padding:6px 10px;gap:6px;order:2;
481
+ }
482
+ .rail-sep{width:1px;height:26px;margin:0 4px;flex-shrink:0}
483
+ .rail-label{display:none}
484
+ .rail-btn{flex-shrink:0}
485
+
486
+ /* Workspace */
487
+ .workspace{min-height:0;order:4}
488
+ .canvas-head{flex-wrap:wrap;gap:8px;padding:8px 12px}
489
+ .canvas-meta{flex-basis:100%;order:3;font-size:10px}
490
+ .canvas-stage{min-height:42vh}
491
+ .canvas-stage img.modern-out-img{max-width:calc(100% - 20px);max-height:52vh}
492
+ .compare-wrap{max-width:calc(100% - 20px);max-height:52vh}
493
+ .canvas-placeholder .cp-sub{font-size:11px;max-width:280px}
494
+
495
+ /* Filmstrip → stacked rows, natural height */
496
+ .filmstrip{flex-direction:column;height:auto;max-height:none}
497
+ .filmstrip-section.inputs{border-right:none;border-bottom:1px solid var(--border)}
498
+ .filmstrip-section{padding:8px 0 8px 12px}
499
+ .fs-thumb{width:68px;height:68px}
500
+ .fs-add{width:68px;height:68px}
501
+ .fs-empty{height:68px}
502
+
503
+ /* Inspector → full width, natural page scroll */
504
+ .inspector{border-left:none;border-top:1px solid var(--border);order:3}
505
+ .inspector-tabs{position:static;background:var(--panel)}
506
+ .insp-tab{padding:13px 8px;font-size:12px}
507
+ .insp-page{overflow-y:visible}
508
+ .insp-section{padding:14px 16px}
509
+ .modern-textarea{font-size:16px} /* prevent iOS zoom-on-focus */
510
+ .btn-run{padding:14px 24px;font-size:15px}
511
+ .suggestion-chip{padding:6px 13px;font-size:12px}
512
+
513
+ /* Status bar */
514
+ .statusbar{flex-wrap:wrap;height:auto;min-height:var(--statusbar-h);padding:4px 10px;gap:2px;order:5}
515
+ .sb-link{display:none}
516
+
517
+ /* Toasts span the phone width */
518
+ .toast-notification{max-width:calc(100vw - 32px);font-size:12px;padding:10px 16px}
519
+
520
+ /* Drop overlay fits small screens */
521
+ .drop-overlay-inner{padding:32px 28px;font-size:15px;border-radius:16px;margin:0 16px}
522
+ }
523
+
524
+ /* ══ Very small phones ══ */
525
+ @media(max-width:400px){
526
+ .menubar-badge.fast{display:none}
527
+ .seg-btn{padding:4px 12px}
528
+ .canvas-stage{min-height:36vh}
529
+ }
530
+ </style>
531
+ </head>
532
+ <body>
533
+ <div class="studio">
534
+
535
+ <!-- ══ Menu bar ══ -->
536
+ <div class="menubar">
537
+ <div class="menubar-logo">
538
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 23c-3.6 0-8-2.69-8-7.5 0-3.5 3-6.5 4.5-8 .27-.27.75-.08.75.28v2.44c0 .42.5.63.72.28C12.28 7.5 13 3 13 1c0-.42.48-.64.8-.35C18 4.5 20 9 20 12c0 5.5-3.5 11-8 11z"/></svg>
539
+ </div>
540
+ <div class="menubar-title">Hy3D Studio <span>/ Image Editor</span></div>
541
+ <span class="menubar-badge">GENERAL</span>
542
+ <span class="menubar-badge fast">4 STEPS DEFAULT</span>
543
+ <div class="menubar-spacer"></div>
544
+ <div class="menubar-status"><span class="dot pending" id="conn-dot"></span><span id="conn-text" role="status" aria-live="polite">connecting…</span></div>
545
+ <a href="https://hy3d.dev" target="_blank" rel="noopener" class="site-btn">
546
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M14 3h7v7"/><path d="M10 14L21 3"/><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"/></svg>
547
+ <span>Hy3D</span>
548
+ </a>
549
+ </div>
550
+
551
+ <!-- ══ Left icon rail ══ -->
552
+ <div class="rail">
553
+ <div class="rail-label">Input</div>
554
+ <button class="rail-btn" id="tb-upload" title="Upload image" aria-label="Upload image">
555
+ <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
556
+ </button>
557
+ <button class="rail-btn" id="tb-remove" title="Remove selected image" aria-label="Remove selected image">
558
+ <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
559
+ </button>
560
+ <button class="rail-btn" id="tb-clear" title="Clear image" aria-label="Clear image">
561
+ <svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
562
+ </button>
563
+ <div class="rail-sep"></div>
564
+ <div class="rail-label">Result</div>
565
+ <button class="rail-btn" id="compare-btn" title="Compare before / after" aria-label="Compare before and after" aria-pressed="false" disabled>
566
+ <svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></svg>
567
+ </button>
568
+ <button class="rail-btn" id="use-as-input-btn" title="Use result as input (chain edits)" aria-label="Use result as input" disabled>
569
+ <svg viewBox="0 0 24 24"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 014-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 01-4 4H3"/></svg>
570
+ </button>
571
+ <button class="rail-btn" id="dl-btn-output" title="Download result" aria-label="Download result" disabled>
572
+ <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
573
+ </button>
574
+ <button class="rail-btn" id="zoom-btn" title="Zoom result" aria-label="Zoom image" disabled>
575
+ <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
576
+ </button>
577
+ </div>
578
+
579
+ <!-- ══ Inspector ══ -->
580
+ <div class="inspector">
581
+ <div class="inspector-tabs">
582
+ <button class="insp-tab active" type="button">Edit</button>
583
+ </div>
584
+
585
+ <!-- Edit page -->
586
+ <div class="insp-page active" id="page-edit">
587
+ <div class="insp-section">
588
+ <div class="insp-section-title"><span>Instruction</span><span class="char-count" id="char-count">0</span></div>
589
+ <textarea id="custom-prompt-input" class="modern-textarea" rows="3" maxlength="1000"
590
+ placeholder="e.g., replace the background, adjust lighting, remove an object…"></textarea>
591
+ </div>
592
+
593
+ <div class="insp-section">
594
+ <div class="insp-section-title"><span>Quick Prompts</span></div>
595
+ <div class="suggestions-wrap" id="suggestions-wrap"></div>
596
+ </div>
597
+
598
+ <div class="insp-section">
599
+ <button id="custom-run-btn" class="btn-run" disabled>
600
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 23c-3.6 0-8-2.69-8-7.5 0-3.5 3-6.5 4.5-8 .27-.27.75-.08.75.28v2.44c0 .42.5.63.72.28C12.28 7.5 13 3 13 1c0-.42.48-.64.8-.35C18 4.5 20 9 20 12c0 5.5-3.5 11-8 11z"/></svg>
601
+ <span id="run-btn-label">Edit Image</span>
602
+ </button>
603
+ <button id="custom-cancel-btn" class="btn-cancel" style="display:none;">Request queue cancellation</button>
604
+ <div class="run-hint">⌘/Ctrl + Enter</div>
605
+ </div>
606
+
607
+ <details class="advanced-section">
608
+ <summary>Advanced</summary>
609
+ <div class="advanced-content">
610
+ <div class="slider-row">
611
+ <label for="custom-seed">Seed</label>
612
+ <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
613
+ <span class="slider-val" id="custom-seed-val">0</span>
614
+ <button class="dice-btn" id="dice-btn" title="Random seed" aria-label="Choose a random seed">
615
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM7.5 18c-.83 0-1.5-.67-1.5-1.5S6.67 15 7.5 15s1.5.67 1.5 1.5S8.33 18 7.5 18zm0-9C6.67 9 6 8.33 6 7.5S6.67 6 7.5 6 9 6.67 9 7.5 8.33 9 7.5 9zm4.5 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm0-9c-.83 0-1.5-.67-1.5-1.5S15.67 6 16.5 6s1.5.67 1.5 1.5S17.33 9 16.5 9z"/></svg>
616
+ </button>
617
+ </div>
618
+ <div class="checkbox-row">
619
+ <input type="checkbox" id="custom-randomize" checked>
620
+ <label for="custom-randomize">Randomize seed</label>
621
+ </div>
622
+ <div class="slider-row">
623
+ <label for="custom-guidance">Guidance</label>
624
+ <input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0">
625
+ <span class="slider-val" id="custom-guidance-val">1.0</span>
626
+ </div>
627
+ <div class="slider-row">
628
+ <label for="custom-steps">Steps</label>
629
+ <input type="range" id="custom-steps" min="1" max="50" step="1" value="4">
630
+ <span class="slider-val" id="custom-steps-val">4</span>
631
+ </div>
632
+ </div>
633
+ </details>
634
+ </div>
635
+ </div>
636
+
637
+ <!-- ══ Workspace ══ -->
638
+ <div class="workspace">
639
+ <div class="canvas-head">
640
+ <div class="seg-control">
641
+ <button class="seg-btn active" id="seg-result">Result</button>
642
+ <button class="seg-btn" id="seg-input">Input</button>
643
+ </div>
644
+ <div class="canvas-meta" id="canvas-meta">No image loaded</div>
645
+ <div class="canvas-actions">
646
+ <span id="out-seed-chip" class="out-seed-chip" title="Click to copy seed"></span>
647
+ </div>
648
+ </div>
649
+
650
+ <div class="canvas-stage" id="output-image-container">
651
+ <div class="modern-loader" id="output-loader">
652
+ <div class="loader-spinner"></div>
653
+ <div class="loader-text" id="loader-text">Processing image&hellip;</div>
654
+ <div class="loader-bar-track"><div class="loader-bar-fill" id="loader-bar-fill"></div></div>
655
+ </div>
656
+
657
+ <div class="canvas-placeholder" id="output-placeholder">
658
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
659
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#33333a" stroke-width="2" stroke-dasharray="4 3"/>
660
+ <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(30,144,255,0.1)" stroke="#33333a" stroke-width="1.5"/>
661
+ <circle cx="28" cy="30" r="6" fill="rgba(30,144,255,0.12)" stroke="#33333a" stroke-width="1.5"/>
662
+ </svg>
663
+ <div class="cp-title">Canvas is empty</div>
664
+ <div class="cp-sub">
665
+ Drop an image anywhere, paste with <kbd>⌘/Ctrl+V</kbd>, or use the rail to upload.
666
+ Then write an instruction and press <kbd>⌘/Ctrl</kbd>+<kbd>↵</kbd>.
667
+ </div>
668
+ </div>
669
+
670
+ <div class="compare-wrap" id="compare-wrap" role="slider" tabindex="0" aria-label="Before and after comparison" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50">
671
+ <img id="compare-after-img" alt="after">
672
+ <div class="compare-top" id="compare-top"><img id="compare-before-img" alt="before"></div>
673
+ <div class="compare-divider" id="compare-divider"><div class="compare-handle">⇄</div></div>
674
+ <span class="compare-label before">Before</span>
675
+ <span class="compare-label after">After</span>
676
+ </div>
677
+ </div>
678
+
679
+ <!-- Filmstrip -->
680
+ <div class="filmstrip">
681
+ <div class="filmstrip-section inputs">
682
+ <div class="filmstrip-label">Inputs <span class="count" id="tb-image-count">0</span></div>
683
+ <div class="filmstrip-row" id="image-gallery-grid"></div>
684
+ </div>
685
+ <div class="filmstrip-section history">
686
+ <div class="filmstrip-label">History <span class="count" id="history-count">0</span></div>
687
+ <div class="filmstrip-row" id="history-strip"></div>
688
+ </div>
689
+ </div>
690
+ </div>
691
+
692
+ <!-- ══ Status bar ══ -->
693
+ <div class="statusbar">
694
+ <div class="sb-section" id="sb-image-count">No inputs</div>
695
+ <div class="sb-right">
696
+ <span class="sb-link">General mode · no LoRA</span>
697
+ <span class="sb-pill" id="sb-status" role="status" aria-live="polite" aria-atomic="true">Connecting…</span>
698
+ </div>
699
+ </div>
700
+
701
+ </div>
702
+
703
+ <input id="custom-file-input" type="file" accept="image/png,image/jpeg,image/webp" style="display:none;" />
704
+
705
+ <!-- Lightbox -->
706
+ <div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Image preview" aria-hidden="true" tabindex="-1">
707
+ <button type="button" class="lightbox-close" id="lightbox-close" aria-label="Close image preview">&times;</button>
708
+ <img id="lightbox-img" alt="Expanded image preview">
709
+ </div>
710
+
711
+ <!-- Global drop overlay -->
712
+ <div class="drop-overlay" id="drop-overlay"><div class="drop-overlay-inner">Drop one image to upload</div></div>
713
+
714
+ <script type="module" src="/hy3d-assets/app-ui.js"></script>
715
+ </body>
716
  </html>
tests/test_space_static.py CHANGED
@@ -1,4 +1,5 @@
1
  import ast
 
2
  import re
3
  import unittest
4
  from pathlib import Path
@@ -84,12 +85,66 @@ class SpaceStaticTests(unittest.TestCase):
84
  self.assertIn('"uses_lora": False', source)
85
  self.assertIn("allowed_edit_modes={BASE_EDIT_MODE_ID}", source)
86
 
87
- def test_static_homepage_has_no_script_or_remote_asset(self):
88
- html = (ROOT / "index.html").read_text(encoding="utf-8").lower()
89
- self.assertNotIn("<script", html)
90
- self.assertNotIn("http://", html)
91
- self.assertNotIn("https://", html)
92
- self.assertNotIn("jsdelivr", html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
  def test_direct_python_dependencies_are_exact(self):
95
  requirements = (ROOT / "requirements.txt").read_text(encoding="utf-8").splitlines()
 
1
  import ast
2
+ import hashlib
3
  import re
4
  import unittest
5
  from pathlib import Path
 
85
  self.assertIn('"uses_lora": False', source)
86
  self.assertIn("allowed_edit_modes={BASE_EDIT_MODE_ID}", source)
87
 
88
+ def test_homepage_is_an_interactive_self_hosted_single_image_editor(self):
89
+ html = (ROOT / "index.html").read_text(encoding="utf-8")
90
+ script = (ROOT / "assets" / "app-ui.js").read_text(encoding="utf-8")
91
+
92
+ self.assertIn('src="/hy3d-assets/app-ui.js"', html)
93
+ self.assertIn('accept="image/png,image/jpeg,image/webp"', html)
94
+ self.assertNotIn(" multiple", html)
95
+ self.assertNotIn("Style / LoRA", html)
96
+ self.assertNotIn("tab-examples", html)
97
+ self.assertNotRegex(html, r'<(?:script|link)[^>]+(?:src|href)=["\']https?://')
98
+ self.assertNotIn("googleapis", html.lower())
99
+ self.assertNotIn("jsdelivr", html.lower())
100
+
101
+ self.assertIn('const GENERAL_MODE_ID = "__base__"', script)
102
+ self.assertIn("images_b64_json: JSON.stringify([beforeB64])", script)
103
+ self.assertIn("lora_adapter: GENERAL_MODE_ID", script)
104
+ self.assertIn('events: ["data", "status"]', script)
105
+ self.assertIn("await currentJob.wait_for_id()", script)
106
+ self.assertIn("await job.return()", script)
107
+ self.assertIn("uses_lora !== false", script)
108
+ self.assertIn("config.loras.length !== 0", script)
109
+ self.assertIn("nextLimits.input_images !== 1", script)
110
+ self.assertIn("Stopped waiting for this job. The backend may still finish it.", script)
111
+ self.assertNotIn("Queued job cancelled", script)
112
+ self.assertNotIn('hideLoader("Cancelled")', script)
113
+ self.assertNotIn("Anime-V2", script)
114
+ self.assertNotIn("Photo-to-Anime", script)
115
+ self.assertNotIn("/load_example", script)
116
+ self.assertNotIn("cfg.examples", script)
117
+
118
+ self.assertIn('class="dot pending"', html)
119
+ self.assertIn("4 STEPS DEFAULT", html)
120
+ self.assertNotIn(".gh-btn", html)
121
+ self.assertIn(".site-btn{padding:6px 10px}", html)
122
+ self.assertLess(html.index('class="inspector"'), html.index('class="workspace"'))
123
+ self.assertIn('id="sb-status" role="status" aria-live="polite"', html)
124
+ self.assertIn('toast.setAttribute("aria-live"', script)
125
+ self.assertIn("studio.inert = true", script)
126
+
127
+ def test_browser_client_is_exactly_vendored(self):
128
+ client_path = ROOT / "assets" / "gradio-client-2.3.1.js"
129
+ self.assertTrue(client_path.is_file())
130
+ digest = hashlib.sha256(client_path.read_bytes()).hexdigest()
131
+ self.assertEqual(digest, "a3d50dc5f3590c5723695abcd85e2009c42a8af08bfc9fa47a7baecc1040c0be")
132
+
133
+ def test_interactive_script_only_references_unique_existing_dom_ids(self):
134
+ html = (ROOT / "index.html").read_text(encoding="utf-8")
135
+ script = (ROOT / "assets" / "app-ui.js").read_text(encoding="utf-8")
136
+ element_ids = re.findall(r'\bid=["\']([^"\']+)["\']', html)
137
+ script_refs = set(re.findall(r'getElementById\(["\']([^"\']+)["\']\)', script))
138
+ script_created_ids = set(re.findall(r'\.id\s*=\s*["\']([^"\']+)["\']', script))
139
+
140
+ self.assertEqual(len(element_ids), len(set(element_ids)))
141
+ self.assertEqual(script_refs - set(element_ids) - script_created_ids, set())
142
+
143
+ def test_ui_assets_and_homepage_are_no_store_and_allowlisted(self):
144
+ source = (ROOT / "app.py").read_text(encoding="utf-8")
145
+ self.assertIn('"Cache-Control": "no-store, max-age=0, must-revalidate"', source)
146
+ self.assertIn('@app.get("/hy3d-assets/{asset_name}"', source)
147
+ self.assertIn('{"app-ui.js", "gradio-client-2.3.1.js"}', source)
148
 
149
  def test_direct_python_dependencies_are_exact(self):
150
  requirements = (ROOT / "requirements.txt").read_text(encoding="utf-8").splitlines()