Fibo-2.0-ComfyUI / examples /pr3-gradio-server-workflow.patch
akhaliq's picture
akhaliq HF Staff
PR #3 patch: gr.Server studio + gr.Workflow for Fibo-2.0-editing
bd7b042 verified
Raw History Blame Contribute Delete
79.9 kB
From 4a7d045978112eaf211d0bac75662a81be096471 Mon Sep 17 00:00:00 2001
From: akhaliq <akhaliq@hf.co>
Date: Thu, 22 Oct 2026 03:35:51 +0000
Subject: [PATCH] gr.Server studio + gr.Workflow canvas on top of the classic
app
- app.py: gr.Server endpoints (/edit, /vgl) + bespoke Photoshop-style studio
at /, gr.Workflow canvas at /workflow (fibo_edit fn node), classic UI
preserved untouched at /classic (classic_app.py)
- studio.html: brush-to-mask, layers, before/after split view, run history,
one-click examples, mobile-responsive
- workflow.json: edit + chained-edit starter graph
- README: gradio 6.30.0 (gr.Server/gr.Workflow), hf_oauth for per-visitor
ZeroGPU quota, document the three surfaces
- pipelines.py / aoti_loader.py / bria_api.py / requirements.txt untouched:
AOTI-accelerated pipeline and Bria VGL prompt writing are shared by all
surfaces via the classic app's render function
---
README.md | 15 +-
app.py | 483 ++++++++++++++------------------
classic_app.py | 320 +++++++++++++++++++++
studio.html | 746 +++++++++++++++++++++++++++++++++++++++++++++++++
workflow.json | 155 ++++++++++
5 files changed, 1442 insertions(+), 277 deletions(-)
create mode 100644 classic_app.py
create mode 100644 studio.html
create mode 100644 workflow.json
diff --git a/README.md b/README.md
index 9f365e1..bcf8d27 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,15 @@ emoji: ⚡
colorFrom: indigo
colorTo: purple
sdk: gradio
-sdk_version: 5.49.1
+sdk_version: 6.30.0
python_version: "3.12" # diffusers from the fibo-2 PR needs >= 3.11
app_file: app.py
pinned: false
short_description: Fibo 2.0 Edit Turbo image editing in 4 steps
+hf_oauth: true
+hf_oauth_scopes:
+ - inference-api
+ - write-repos
models:
- briaai/Fibo-2.0-edit-turbo
- briaai/Fibo-2.0-dev # listed so the Space shows on the Dev model page too
@@ -18,4 +22,13 @@ Fibo 2.0 Edit Turbo on ZeroGPU: instruction-based image editing in 4 guidance-fr
reference images and an optional mask. The JSON instructions come from the Bria API's
`/v2/vgl/edit`, written with Fibo 2.0's VGL prompts.
+Three surfaces:
+
+- **`/` — Studio**: a bespoke Photoshop-style editor (`gr.Server` API + custom UI): brush-to-mask,
+ layers, before/after split view, run history, one-click examples. Edits call the same `render`
+ as the classic app (the AOTI-accelerated pipeline in `pipelines.py` is shared by every surface).
+- **`/workflow` — Workflow canvas** (`gr.Workflow`): node-based edit pipelines you can rewire,
+ chain, and save. Run nodes one at a time with ▶ — up-to-date upstream results are reused.
+- **`/classic` — the classic UI**, unchanged.
+
Space secrets: `HF_TOKEN` (read access to the gated model), `BRIA_API_TOKEN` (the Bria API).
diff --git a/app.py b/app.py
index 096ce49..72a07b1 100644
--- a/app.py
+++ b/app.py
@@ -1,321 +1,252 @@
-import os
-import json
+"""FIBO 2.0 Edit Turbo — gr.Server studio + gr.Workflow canvas on top of the classic app.
+
+The classic Gradio UI is preserved untouched in classic_app.py (mounted at /classic);
+this adds a bespoke Photoshop-style studio at / (gr.Server API endpoints) and a
+node-based workflow canvas at /workflow. All GPU work goes through the same
+`render` function as the classic app, so the AOTI-accelerated pipeline loading in
+pipelines.py is shared by every surface.
+"""
+
import base64
-import random
-from contextlib import contextmanager
+import json
+import os
+import tempfile
+from urllib.parse import unquote, urlparse
import gradio as gr
-import numpy as np
-import spaces
-import torch
+import uvicorn
+from fastapi import Request
+from fastapi.responses import HTMLResponse, RedirectResponse
+from fastapi.staticfiles import StaticFiles
+from gradio_client import utils as client_utils
from PIL import Image
import bria_api
-import pipelines
-from pipelines import GUIDANCE_SCALE, MAX_STEPS, STEPS
+from classic_app import ( # noqa: F401 (classic_app loads the pipeline once, at import)
+ MAX_SEED,
+ as_prompt,
+ collect_references,
+ demo as classic_demo,
+ pretty,
+ render,
+ resolve_seed,
+ user_errors,
+)
+from pipelines import MAX_STEPS, STEPS
-MAX_SEED = np.iinfo(np.int32).max
-MAX_REFERENCES = bria_api.MAX_REFERENCES
+APP_DIR = os.path.dirname(os.path.abspath(__file__))
-pipe = pipelines.load_pipeline()
-# A 4-step edit of one 1024 px image measured 7 s on an L40S; references add tokens.
-GPU_DURATION = 60
+def _as_data_uri(path: str) -> str:
+ """Inline a result image: Server mode's /gradio_api/file= route 403s the
+ tempdir even with allowed_paths, so the page gets data URIs instead."""
+ with open(path, "rb") as f:
+ return "data:image/png;base64," + base64.b64encode(f.read()).decode()
-@spaces.GPU(duration=GPU_DURATION, size=pipelines.GPU_SIZE)
-def render(json_prompt, seed, steps, references, mask=None):
- """An edit of references[0], with the rest as extra references."""
- with torch.inference_mode():
- return pipe(
- prompt=json_prompt,
- image=references[0] if len(references) == 1 else references,
- mask=mask,
- num_inference_steps=int(steps),
- guidance_scale=GUIDANCE_SCALE,
- generator=torch.Generator(device="cuda").manual_seed(int(seed)),
- ).images[0]
+def _local_path(value) -> str:
+ """Turn whatever the canvas/API client sends into a path this process can open."""
+ if isinstance(value, str):
+ return value
+ if not isinstance(value, dict):
+ raise gr.Error(f"Expected an image, got {type(value).__name__}")
+ if path := value.get("path"):
+ return path
+ url = value.get("url") or ""
+ if "/gradio_api/file=" in url:
+ return unquote(url.split("/gradio_api/file=", 1)[1])
+ if url.startswith(("http://", "https://")):
+ return client_utils.download_tmp_copy_of_file(url)
+ return unquote(urlparse(url).path)
-# --------------------------------------------------------------------------- helpers
+def _open_rgb(value) -> Image.Image:
+ return Image.open(_local_path(value)).convert("RGB")
-@contextmanager
-def user_errors():
- """Prompt-service failures surface as messages, not stack traces."""
- try:
- yield
- except bria_api.BriaAPIError as error:
- raise gr.Error(str(error))
+def _mask_from_upload(value, source: Image.Image):
+ """A white-on-black mask image from the studio's brush export, sized to the source."""
+ if value is None:
+ return None
+ mask = Image.open(_local_path(value)).convert("L").point(lambda v: 255 if v > 8 else 0)
+ if mask.getbbox() is None:
+ return None
+ return mask.resize(source.size, Image.NEAREST)
-def resolve_seed(seed, randomize):
- # The /v2/vgl endpoints reject seed 0 (they take 1..int32 max).
- return random.randint(1, MAX_SEED) if randomize else max(int(seed), 1)
+def _save_tmp(image: Image.Image) -> str:
+ path = os.path.join(tempfile.gettempdir(), f"fibo_edit_{os.urandom(8).hex()}.png")
+ image.save(path)
+ return path
-def pretty(json_text):
- """Format for the code box, leaving invalid text untouched."""
- try:
- return json.dumps(json.loads(json_text), indent=4)
- except (TypeError, ValueError):
- return json_text or ""
+def _as_media(path: str) -> dict:
+ """The dict shape the workflow canvas renders media from."""
+ return {
+ "path": path,
+ "url": f"/gradio_api/file={client_utils.encode_file_path(path)}",
+ "is_file": True,
+ }
-def as_prompt(json_value):
- """The code box returns a string; a fresh API response may already be a dict."""
- if isinstance(json_value, (dict, list)):
- return json.dumps(json_value, indent=4)
- text = str(json_value or "")
- try:
- if isinstance(json.loads(text), dict):
- return text
- except ValueError:
- pass
- raise gr.Error("The JSON instruction is not a valid JSON object. Fix it, or clear it to regenerate it.")
+# ------------------------------------------------------------------ workflow fn
-def collect_references(*images):
- """Gap-free reference list; slot order is what <image_1>..<image_N> mean."""
- return [image.convert("RGB") for image in images if image is not None]
+def fibo_edit(image, json_instruction: str, seed: str = "42", steps: str = str(STEPS)) -> dict:
+ """Edit an image with Fibo 2.0 Edit Turbo (4 guidance-free steps)."""
+ source = _open_rgb(image)
+ json_prompt = as_prompt(json_instruction)
+ result = render(json_prompt, int(float(seed)), int(float(steps)), [source])
+ return _as_media(_save_tmp(result))
-def mask_from_editor(editor_value, source):
- """Brush layer -> 'L' mask sized to the source. White means repaint here."""
- if not editor_value:
- return None
- layers = editor_value.get("layers") or []
- if not layers:
- return None
- painted = layers[0].convert("RGBA")
- mask = painted.getchannel("A").point(lambda value: 255 if value > 8 else 0)
- if mask.getbbox() is None:
- return None
- return mask.resize(source.size, Image.NEAREST)
+# ------------------------------------------------------------------ studio server
+server = gr.Server(title="FIBO 2.0 Edit Turbo Studio")
-def busy():
- return gr.update(interactive=False)
+# OAuth routes attach only on the mounted workflow app (at /workflow/...), but
+# gradio's login buttons and the OAuth redirect_uri point at the Space root —
+# forward the three auth paths so sign-in completes instead of 404ing. The login
+# itself opens in a new tab (HF auth pages refuse to render inside an iframe).
+@server.get("/login/huggingface", response_class=HTMLResponse)
+def _oauth_login(request: Request):
+ qs = str(request.url.query)
+ target = "/workflow/login/huggingface" + ("?" + qs if qs else "")
+ return (
+ '<!DOCTYPE html><html><body style="'
+ 'font-family:Inter,system-ui,sans-serif;background:#0e0e10;color:rgba(255,255,255,0.87);'
+ 'display:flex;flex-direction:column;align-items:center;justify-content:center;'
+ 'height:100vh;margin:0;gap:14px;text-align:center">'
+ "<div style='font-size:1.4rem'>⚡</div>"
+ f"<a href='{target}' target='_blank' rel='noopener' style='"
+ 'background:linear-gradient(90deg,#5300C9,#8a3dff);color:white;'
+ "padding:12px 26px;border-radius:999px;text-decoration:none;font-weight:700'>"
+ 'Sign in with Hugging Face →</a>'
+ "<p style='color:rgba(255,255,255,0.5);font-size:0.82rem;margin:0'>Opens in a new tab — this page stays here.</p>"
+ f"<a href='/workflow/?ui=canvas' style='color:#8a3dff;font-size:0.82rem;text-decoration:none'>"
+ 'Signed in? Back to the canvas ↻</a>'
+ '</body></html>'
+ )
-def idle():
- return gr.update(interactive=True)
+@server.get("/login/callback")
+def _oauth_callback(request: Request):
+ qs = str(request.url.query)
+ return RedirectResponse("/workflow/login/callback" + ("?" + qs if qs else ""))
-# ------------------------------------------------------------------------------- edit
+@server.get("/logout")
+def _oauth_logout(request: Request):
+ qs = str(request.url.query)
+ return RedirectResponse("/workflow/logout" + ("?" + qs if qs else ""))
-def check_edit_inputs(references, mask):
- if not references:
- raise gr.Error("Upload a source image first.")
- if mask is not None and len(references) != 1:
- raise gr.Error(
- "A mask only works with a single reference image. Clear the extra references, "
- "or clear the mask."
- )
+
+@server.get("/", response_class=HTMLResponse)
+def _studio_page():
+ with open(os.path.join(APP_DIR, "studio.html"), encoding="utf-8") as f:
+ return f.read()
-def run_edit(instruction, source, ref2, ref3, ref4, mask_editor, json_prompt, seed, randomize,
- steps, progress=gr.Progress(track_tqdm=True)):
- seed = resolve_seed(seed, randomize)
- references = collect_references(source, ref2, ref3, ref4)
- mask = mask_from_editor(mask_editor, source) if source is not None else None
- check_edit_inputs(references, mask)
+_examples_dir = os.path.join(APP_DIR, "examples")
+if os.path.isdir(_examples_dir):
+ server.mount("/examples", StaticFiles(directory=_examples_dir), name="examples")
+
+@server.api(name="vgl", concurrency_limit=None)
+def api_vgl(
+ instruction: str,
+ source: str,
+ ref2: str | None = None,
+ ref3: str | None = None,
+ ref4: str | None = None,
+ mask: str | None = None,
+ seed: float = 42,
+) -> dict:
+ """Write the edit's structured JSON instruction with the Bria VGL API."""
+ images = [_open_rgb(p) for p in (source, ref2, ref3, ref4) if p]
+ mask_img = _mask_from_upload(mask, images[0]) if mask and len(images) == 1 else None
+ with user_errors():
+ result = bria_api.vgl_edit(
+ instruction=instruction, images=images, mask=mask_img, seed=int(seed)
+ )
+ return {"json_instruction": pretty(result) if isinstance(result, str) else result}
+
+
+@server.api(name="edit", concurrency_limit=None)
+def api_edit(
+ source: str,
+ instruction: str = "",
+ json_instruction: str = "",
+ ref2: str | None = None,
+ ref3: str | None = None,
+ ref4: str | None = None,
+ mask: str | None = None,
+ seed: float = 42,
+ randomize: bool = True,
+ steps: float = STEPS,
+) -> dict:
+ """Edit the source (+ up to 3 references, optional mask) with Fibo 2.0 Edit Turbo."""
+ if source is None:
+ raise gr.Error("Upload a source image first.")
+ images = [_open_rgb(p) for p in (source, ref2, ref3, ref4) if p]
+ mask_img = _mask_from_upload(mask, images[0]) if mask else None
+ if mask_img is not None and len(images) != 1:
+ raise gr.Error("A mask only works with a single reference image.")
+ seed_value = resolve_seed(seed, randomize)
+ json_prompt = (json_instruction or "").strip()
if not json_prompt:
with user_errors():
json_prompt = bria_api.vgl_edit(
- instruction=instruction, images=references, mask=mask, seed=seed
+ instruction=instruction, images=images, mask=mask_img, seed=seed_value
)
json_prompt = as_prompt(json_prompt)
+ result = render(json_prompt, seed_value, int(steps), images, mask=mask_img)
+ return {
+ "image_data": _as_data_uri(_save_tmp(result)),
+ "seed": seed_value,
+ "json_instruction": pretty(json_prompt),
+ }
- image = render(json_prompt, seed, steps, references, mask=mask)
- return image, seed, pretty(json_prompt)
-
-
-def invalidate_edit_json():
- """A changed image, instruction or mask makes the generated JSON instruction stale."""
- return ""
-
-
-# --------------------------------------------------------------------------------- ui
-
-def _load_logo():
- """Embed the bundled logo as a data URI so the header never depends on an external CDN."""
- try:
- with open(os.path.join(os.path.dirname(__file__), "bria_logo.png"), "rb") as f:
- return "data:image/png;base64," + base64.b64encode(f.read()).decode("utf-8")
- except Exception:
- return "https://cdn.sanity.io/images/zppcnj0l/production/d0f4aef10abdcc4bacaebeade789ea8c3e453882-210x132.png"
-
-
-BRIA_LOGO = _load_logo()
-
-# Bria brand theme (purple #5300C9, magenta accent).
-bria_theme = gr.themes.Soft(
- primary_hue=gr.themes.Color(
- c50="#f3ebff", c100="#e4d3ff", c200="#c9a6ff", c300="#a86bff",
- c400="#8a3dff", c500="#5300C9", c600="#4700ab", c700="#37008a",
- c800="#28006b", c900="#1c0050", c950="#120038",
- ),
- secondary_hue=gr.themes.Color(
- c50="#ffe9f4", c100="#ffd0e6", c200="#ffa1cd", c300="#ff6fb2",
- c400="#f5399a", c500="#e2138a", c600="#c00b74", c700="#99095c",
- c800="#730745", c900="#520531", c950="#340120",
- ),
- neutral_hue="slate",
- font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
- radius_size=gr.themes.sizes.radius_lg,
-).set(
- body_background_fill="#f6f5fb",
- body_background_fill_dark="#0f0b1a",
- block_background_fill="white",
- block_background_fill_dark="#1a1330",
- block_border_width="1px",
- block_shadow="0 1px 3px rgba(28,0,80,0.06), 0 8px 24px rgba(28,0,80,0.05)",
- button_primary_background_fill="linear-gradient(90deg, #5300C9 0%, #8a3dff 100%)",
- button_primary_background_fill_hover="linear-gradient(90deg, #4700ab 0%, #7a2dff 100%)",
- button_primary_text_color="white",
- button_large_radius="12px",
-)
-css = """
-#col-container { margin: 0 auto; max-width: 1000px; }
-#json_instruction { max-height: 800px; }
-
-#bria-header, #bria-footer { max-width: 1000px; margin: 0 auto; }
-#bria-header {
- display: flex; align-items: center; gap: 18px;
- padding: 22px 26px; margin-bottom: 10px;
- background: linear-gradient(120deg, #ffffff 0%, #f3ecff 100%);
- border: 1px solid rgba(83, 0, 201, 0.12); border-radius: 18px;
-}
-.dark #bria-header { background: linear-gradient(120deg, #1a1330 0%, #241640 100%); border-color: rgba(138, 61, 255, 0.25); }
-#bria-header img { height: 52px; width: auto; }
-#bria-header .title-block h1 { margin: 0; font-size: 1.7rem; font-weight: 800; letter-spacing: -0.02em; color: #1c0050; }
-.dark #bria-header .title-block h1 { color: #f0eafc; }
-#bria-header .title-block p { margin: 4px 0 0 0; font-size: 0.95rem; color: #6b6580; }
-.dark #bria-header .title-block p { color: #b3abc9; }
-#bria-header .pill {
- display: inline-block; margin-top: 8px; padding: 3px 12px;
- font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
- color: #5300C9; background: rgba(83, 0, 201, 0.09); border-radius: 999px;
-}
-.dark #bria-header .pill { color: #c9a6ff; background: rgba(138, 61, 255, 0.15); }
-
-#bria-footer {
- margin-top: 24px; padding-top: 16px; border-top: 1px solid rgba(83, 0, 201, 0.12);
- text-align: center; font-size: 0.88rem; color: #6b6580; line-height: 1.7;
-}
-#bria-footer a { color: #5300C9; text-decoration: none; font-weight: 600; }
-#bria-footer a:hover { text-decoration: underline; }
-.dark #bria-footer { color: #b3abc9; border-color: rgba(138, 61, 255, 0.22); }
-.dark #bria-footer a { color: #c9a6ff; }
-"""
+# ------------------------------------------------------------------ workflow canvas
-with gr.Blocks(css=css, theme=bria_theme, title="BRIA FIBO 2.0 Edit Turbo") as demo:
-
- gr.HTML(f"""
- <div id="bria-header">
- <img src="{BRIA_LOGO}" alt="BRIA logo" />
- <div class="title-block">
- <h1>FIBO 2.0 Edit Turbo</h1>
- <p>8B image editing in 4 steps, driven by structured JSON instructions — with references and masks.</p>
- <span class="pill">Open model · 8B · Edit Turbo</span>
- </div>
- </div>
- """)
-
- with gr.Row(elem_id="col-container"):
- with gr.Column(scale=1):
- with gr.Tabs():
-
- # ---------------------------------------------------------- edit
- with gr.Tab("Edit", id="edit_tab"):
- gr.Markdown(
- "Upload a source image and describe the edit. Add up to three more "
- "references — the instruction refers to them in slot order as "
- "`<image_1>` … `<image_4>`."
- )
- with gr.Row():
- edit_source = gr.Image(label="1 · Source (required)", type="pil")
- edit_ref2 = gr.Image(label="2 · Reference", type="pil")
- with gr.Row():
- edit_ref3 = gr.Image(label="3 · Reference", type="pil")
- edit_ref4 = gr.Image(label="4 · Reference", type="pil")
-
- with gr.Accordion(
- "Mask — paint the area to repaint (single reference only)", open=False
- ):
- edit_mask = gr.ImageEditor(
- label="Brush over the region you want changed",
- type="pil",
- brush=gr.Brush(colors=["#ffffff"], color_mode="fixed"),
- )
-
- edit_instruction = gr.Textbox(
- label="Edit instruction",
- placeholder="make the sky dramatic and stormy", lines=2,
- )
- edit_json = gr.Code(
- label="JSON Instruction", language="json", wrap_lines=True,
- lines=10, elem_id="json_instruction",
- )
- with gr.Accordion("Advanced Settings", open=False):
- with gr.Row():
- edit_seed = gr.Slider(
- label="Seed", minimum=1, maximum=MAX_SEED, step=1, value=1
- )
- edit_randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
- edit_steps = gr.Slider(
- label="Steps", minimum=1, maximum=MAX_STEPS, step=1, value=STEPS,
- info="Fibo 2.0 Edit Turbo is distilled for 4 guidance-free steps.",
- )
- edit_run_btn = gr.Button("Generate Edit", variant="primary")
-
- gr.Examples(
- examples=[
- ["examples/bicycle_wall.jpg", "Change the bicycle color to bright red"],
- ["examples/businessman_suit.jpg", "Add round glasses"],
- ["examples/cat_tabby.jpg", "Put a small party hat on the cat"],
- ],
- inputs=[edit_source, edit_instruction],
- label="Example edits",
- )
-
- with gr.Column(scale=1):
- result = gr.Image(label="output")
-
- # -------------------------------------------------------------------- wiring
-
-
- edit_run_btn.click(
- fn=busy, inputs=None, outputs=edit_run_btn,
- ).then(
- fn=run_edit,
- inputs=[edit_instruction, edit_source, edit_ref2, edit_ref3, edit_ref4, edit_mask,
- edit_json, edit_seed, edit_randomize_seed, edit_steps],
- outputs=[result, edit_seed, edit_json],
- ).then(
- fn=idle, inputs=None, outputs=edit_run_btn,
+workflow_demo = gr.Workflow(
+ graph="workflow.json",
+ bind={"fibo_edit": fibo_edit},
+)
+workflow_demo.title = "FIBO 2.0 Edit Turbo Workflow"
+
+# Mounts: the specific paths first, the studio's catch-all last.
+server = gr.mount_gradio_app(server, workflow_demo, path="/workflow", ssr_mode=False)
+server = gr.mount_gradio_app(server, classic_demo, path="/classic", ssr_mode=False)
+
+# The @server.api endpoints are deferred until Server.launch(), which also fires
+# the ZeroGPU registration hook (pack weights + startup report) through the
+# patched Blocks.launch path. Launch on a loopback port for registration, then
+# serve the same app via uvicorn.
+_port_env = os.environ.get("GRADIO_SERVER_PORT")
+os.environ["GRADIO_SERVER_PORT"] = "7861"
+try:
+ server.launch(
+ prevent_thread_lock=True,
+ server_name="127.0.0.1",
+ server_port=7861,
+ quiet=True,
+ show_error=False,
+ ssr_mode=False,
+ allowed_paths=[tempfile.gettempdir()],
)
-
- # "Generate Edit" reuses the JSON box when it isn't empty, so anything that makes it stale clears it.
- for slot in (edit_source, edit_ref2, edit_ref3, edit_ref4, edit_instruction, edit_mask):
- slot.change(fn=invalidate_edit_json, inputs=None, outputs=[edit_json])
-
- # The mask must match the source, so seed the editor from it.
- edit_source.change(fn=lambda image: image, inputs=[edit_source], outputs=[edit_mask])
-
- gr.HTML("""
- <div id="bria-footer">
- <p>
- Edits run <b>Fibo 2.0 Edit Turbo</b> (4 guidance-free steps, up to 4 reference images).
- JSON instructions by the Bria API (/v2/vgl/edit). Trained on licensed data.<br/>
- <a href="https://huggingface.co/briaai/Fibo-2.0-edit-turbo" target="_blank">Model</a> ·
- <a href="https://arxiv.org/abs/2511.06876" target="_blank">arXiv</a>
- </p>
- </div>
- """)
-
-demo.queue().launch(ssr_mode=False, show_error=True)
+finally:
+ if _port_env is None:
+ del os.environ["GRADIO_SERVER_PORT"]
+ else:
+ os.environ["GRADIO_SERVER_PORT"] = _port_env
+
+try:
+ server.close()
+except Exception:
+ pass
+
+if __name__ == "__main__":
+ uvicorn.run(server, host="0.0.0.0", port=7860)
diff --git a/classic_app.py b/classic_app.py
new file mode 100644
index 0000000..ffa8cbe
--- /dev/null
+++ b/classic_app.py
@@ -0,0 +1,320 @@
+import os
+import json
+import base64
+import random
+from contextlib import contextmanager
+
+import gradio as gr
+import numpy as np
+import spaces
+import torch
+from PIL import Image
+
+import bria_api
+import pipelines
+from pipelines import GUIDANCE_SCALE, MAX_STEPS, STEPS
+
+MAX_SEED = np.iinfo(np.int32).max
+MAX_REFERENCES = bria_api.MAX_REFERENCES
+
+pipe = pipelines.load_pipeline()
+
+# A 4-step edit of one 1024 px image measured 7 s on an L40S; references add tokens.
+GPU_DURATION = 60
+
+
+@spaces.GPU(duration=GPU_DURATION, size=pipelines.GPU_SIZE)
+def render(json_prompt, seed, steps, references, mask=None):
+ """An edit of references[0], with the rest as extra references."""
+ with torch.inference_mode():
+ return pipe(
+ prompt=json_prompt,
+ image=references[0] if len(references) == 1 else references,
+ mask=mask,
+ num_inference_steps=int(steps),
+ guidance_scale=GUIDANCE_SCALE,
+ generator=torch.Generator(device="cuda").manual_seed(int(seed)),
+ ).images[0]
+
+
+# --------------------------------------------------------------------------- helpers
+
+
+@contextmanager
+def user_errors():
+ """Prompt-service failures surface as messages, not stack traces."""
+ try:
+ yield
+ except bria_api.BriaAPIError as error:
+ raise gr.Error(str(error))
+
+
+def resolve_seed(seed, randomize):
+ # The /v2/vgl endpoints reject seed 0 (they take 1..int32 max).
+ return random.randint(1, MAX_SEED) if randomize else max(int(seed), 1)
+
+
+def pretty(json_text):
+ """Format for the code box, leaving invalid text untouched."""
+ try:
+ return json.dumps(json.loads(json_text), indent=4)
+ except (TypeError, ValueError):
+ return json_text or ""
+
+
+def as_prompt(json_value):
+ """The code box returns a string; a fresh API response may already be a dict."""
+ if isinstance(json_value, (dict, list)):
+ return json.dumps(json_value, indent=4)
+ text = str(json_value or "")
+ try:
+ if isinstance(json.loads(text), dict):
+ return text
+ except ValueError:
+ pass
+ raise gr.Error("The JSON instruction is not a valid JSON object. Fix it, or clear it to regenerate it.")
+
+
+def collect_references(*images):
+ """Gap-free reference list; slot order is what <image_1>..<image_N> mean."""
+ return [image.convert("RGB") for image in images if image is not None]
+
+
+def mask_from_editor(editor_value, source):
+ """Brush layer -> 'L' mask sized to the source. White means repaint here."""
+ if not editor_value:
+ return None
+ layers = editor_value.get("layers") or []
+ if not layers:
+ return None
+ painted = layers[0].convert("RGBA")
+ mask = painted.getchannel("A").point(lambda value: 255 if value > 8 else 0)
+ if mask.getbbox() is None:
+ return None
+ return mask.resize(source.size, Image.NEAREST)
+
+
+def busy():
+ return gr.update(interactive=False)
+
+
+def idle():
+ return gr.update(interactive=True)
+
+
+# ------------------------------------------------------------------------------- edit
+
+
+def check_edit_inputs(references, mask):
+ if not references:
+ raise gr.Error("Upload a source image first.")
+ if mask is not None and len(references) != 1:
+ raise gr.Error(
+ "A mask only works with a single reference image. Clear the extra references, "
+ "or clear the mask."
+ )
+
+
+def run_edit(instruction, source, ref2, ref3, ref4, mask_editor, json_prompt, seed, randomize,
+ steps, progress=gr.Progress(track_tqdm=True)):
+ seed = resolve_seed(seed, randomize)
+ references = collect_references(source, ref2, ref3, ref4)
+ mask = mask_from_editor(mask_editor, source) if source is not None else None
+ check_edit_inputs(references, mask)
+
+ if not json_prompt:
+ with user_errors():
+ json_prompt = bria_api.vgl_edit(
+ instruction=instruction, images=references, mask=mask, seed=seed
+ )
+ json_prompt = as_prompt(json_prompt)
+
+ image = render(json_prompt, seed, steps, references, mask=mask)
+ return image, seed, pretty(json_prompt)
+
+
+def invalidate_edit_json():
+ """A changed image, instruction or mask makes the generated JSON instruction stale."""
+ return ""
+
+
+# --------------------------------------------------------------------------------- ui
+
+def _load_logo():
+ """Embed the bundled logo as a data URI so the header never depends on an external CDN."""
+ try:
+ with open(os.path.join(os.path.dirname(__file__), "bria_logo.png"), "rb") as f:
+ return "data:image/png;base64," + base64.b64encode(f.read()).decode("utf-8")
+ except Exception:
+ return "https://cdn.sanity.io/images/zppcnj0l/production/d0f4aef10abdcc4bacaebeade789ea8c3e453882-210x132.png"
+
+
+BRIA_LOGO = _load_logo()
+
+# Bria brand theme (purple #5300C9, magenta accent).
+bria_theme = gr.themes.Soft(
+ primary_hue=gr.themes.Color(
+ c50="#f3ebff", c100="#e4d3ff", c200="#c9a6ff", c300="#a86bff",
+ c400="#8a3dff", c500="#5300C9", c600="#4700ab", c700="#37008a",
+ c800="#28006b", c900="#1c0050", c950="#120038",
+ ),
+ secondary_hue=gr.themes.Color(
+ c50="#ffe9f4", c100="#ffd0e6", c200="#ffa1cd", c300="#ff6fb2",
+ c400="#f5399a", c500="#e2138a", c600="#c00b74", c700="#99095c",
+ c800="#730745", c900="#520531", c950="#340120",
+ ),
+ neutral_hue="slate",
+ font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
+ radius_size=gr.themes.sizes.radius_lg,
+).set(
+ body_background_fill="#f6f5fb",
+ body_background_fill_dark="#0f0b1a",
+ block_background_fill="white",
+ block_background_fill_dark="#1a1330",
+ block_border_width="1px",
+ block_shadow="0 1px 3px rgba(28,0,80,0.06), 0 8px 24px rgba(28,0,80,0.05)",
+ button_primary_background_fill="linear-gradient(90deg, #5300C9 0%, #8a3dff 100%)",
+ button_primary_background_fill_hover="linear-gradient(90deg, #4700ab 0%, #7a2dff 100%)",
+ button_primary_text_color="white",
+ button_large_radius="12px",
+)
+
+css = """
+#col-container { margin: 0 auto; max-width: 1000px; }
+#json_instruction { max-height: 800px; }
+
+#bria-header, #bria-footer { max-width: 1000px; margin: 0 auto; }
+#bria-header {
+ display: flex; align-items: center; gap: 18px;
+ padding: 22px 26px; margin-bottom: 10px;
+ background: linear-gradient(120deg, #ffffff 0%, #f3ecff 100%);
+ border: 1px solid rgba(83, 0, 201, 0.12); border-radius: 18px;
+}
+.dark #bria-header { background: linear-gradient(120deg, #1a1330 0%, #241640 100%); border-color: rgba(138, 61, 255, 0.25); }
+#bria-header img { height: 52px; width: auto; }
+#bria-header .title-block h1 { margin: 0; font-size: 1.7rem; font-weight: 800; letter-spacing: -0.02em; color: #1c0050; }
+.dark #bria-header .title-block h1 { color: #f0eafc; }
+#bria-header .title-block p { margin: 4px 0 0 0; font-size: 0.95rem; color: #6b6580; }
+.dark #bria-header .title-block p { color: #b3abc9; }
+#bria-header .pill {
+ display: inline-block; margin-top: 8px; padding: 3px 12px;
+ font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
+ color: #5300C9; background: rgba(83, 0, 201, 0.09); border-radius: 999px;
+}
+.dark #bria-header .pill { color: #c9a6ff; background: rgba(138, 61, 255, 0.15); }
+
+#bria-footer {
+ margin-top: 24px; padding-top: 16px; border-top: 1px solid rgba(83, 0, 201, 0.12);
+ text-align: center; font-size: 0.88rem; color: #6b6580; line-height: 1.7;
+}
+#bria-footer a { color: #5300C9; text-decoration: none; font-weight: 600; }
+#bria-footer a:hover { text-decoration: underline; }
+.dark #bria-footer { color: #b3abc9; border-color: rgba(138, 61, 255, 0.22); }
+.dark #bria-footer a { color: #c9a6ff; }
+"""
+
+with gr.Blocks(css=css, theme=bria_theme, title="BRIA FIBO 2.0 Edit Turbo") as demo:
+
+ gr.HTML(f"""
+ <div id="bria-header">
+ <img src="{BRIA_LOGO}" alt="BRIA logo" />
+ <div class="title-block">
+ <h1>FIBO 2.0 Edit Turbo</h1>
+ <p>8B image editing in 4 steps, driven by structured JSON instructions — with references and masks.</p>
+ <span class="pill">Open model · 8B · Edit Turbo</span>
+ </div>
+ </div>
+ """)
+
+ with gr.Row(elem_id="col-container"):
+ with gr.Column(scale=1):
+ with gr.Tabs():
+
+ # ---------------------------------------------------------- edit
+ with gr.Tab("Edit", id="edit_tab"):
+ gr.Markdown(
+ "Upload a source image and describe the edit. Add up to three more "
+ "references — the instruction refers to them in slot order as "
+ "`<image_1>` … `<image_4>`."
+ )
+ with gr.Row():
+ edit_source = gr.Image(label="1 · Source (required)", type="pil")
+ edit_ref2 = gr.Image(label="2 · Reference", type="pil")
+ with gr.Row():
+ edit_ref3 = gr.Image(label="3 · Reference", type="pil")
+ edit_ref4 = gr.Image(label="4 · Reference", type="pil")
+
+ with gr.Accordion(
+ "Mask — paint the area to repaint (single reference only)", open=False
+ ):
+ edit_mask = gr.ImageEditor(
+ label="Brush over the region you want changed",
+ type="pil",
+ brush=gr.Brush(colors=["#ffffff"], color_mode="fixed"),
+ )
+
+ edit_instruction = gr.Textbox(
+ label="Edit instruction",
+ placeholder="make the sky dramatic and stormy", lines=2,
+ )
+ edit_json = gr.Code(
+ label="JSON Instruction", language="json", wrap_lines=True,
+ lines=10, elem_id="json_instruction",
+ )
+ with gr.Accordion("Advanced Settings", open=False):
+ with gr.Row():
+ edit_seed = gr.Slider(
+ label="Seed", minimum=1, maximum=MAX_SEED, step=1, value=1
+ )
+ edit_randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
+ edit_steps = gr.Slider(
+ label="Steps", minimum=1, maximum=MAX_STEPS, step=1, value=STEPS,
+ info="Fibo 2.0 Edit Turbo is distilled for 4 guidance-free steps.",
+ )
+ edit_run_btn = gr.Button("Generate Edit", variant="primary")
+
+ gr.Examples(
+ examples=[
+ ["examples/bicycle_wall.jpg", "Change the bicycle color to bright red"],
+ ["examples/businessman_suit.jpg", "Add round glasses"],
+ ["examples/cat_tabby.jpg", "Put a small party hat on the cat"],
+ ],
+ inputs=[edit_source, edit_instruction],
+ label="Example edits",
+ )
+
+ with gr.Column(scale=1):
+ result = gr.Image(label="output")
+
+ # -------------------------------------------------------------------- wiring
+
+
+ edit_run_btn.click(
+ fn=busy, inputs=None, outputs=edit_run_btn,
+ ).then(
+ fn=run_edit,
+ inputs=[edit_instruction, edit_source, edit_ref2, edit_ref3, edit_ref4, edit_mask,
+ edit_json, edit_seed, edit_randomize_seed, edit_steps],
+ outputs=[result, edit_seed, edit_json],
+ ).then(
+ fn=idle, inputs=None, outputs=edit_run_btn,
+ )
+
+ # "Generate Edit" reuses the JSON box when it isn't empty, so anything that makes it stale clears it.
+ for slot in (edit_source, edit_ref2, edit_ref3, edit_ref4, edit_instruction, edit_mask):
+ slot.change(fn=invalidate_edit_json, inputs=None, outputs=[edit_json])
+
+ # The mask must match the source, so seed the editor from it.
+ edit_source.change(fn=lambda image: image, inputs=[edit_source], outputs=[edit_mask])
+
+ gr.HTML("""
+ <div id="bria-footer">
+ <p>
+ Edits run <b>Fibo 2.0 Edit Turbo</b> (4 guidance-free steps, up to 4 reference images).
+ JSON instructions by the Bria API (/v2/vgl/edit). Trained on licensed data.<br/>
+ <a href="https://huggingface.co/briaai/Fibo-2.0-edit-turbo" target="_blank">Model</a> ·
+ <a href="https://arxiv.org/abs/2511.06876" target="_blank">arXiv</a>
+ </p>
+ </div>
+ """)
+
diff --git a/studio.html b/studio.html
new file mode 100644
index 0000000..892c465
--- /dev/null
+++ b/studio.html
@@ -0,0 +1,746 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<title>FIBO 2.0 Edit Turbo Studio</title>
+<style>
+ :root {
+ --bg: #0e0e10; --panel: #16171a; --panel2: #101114; --raised: #1e2024;
+ --border: rgba(255,255,255,0.07);
+ --text: rgba(255,255,255,0.87); --muted: rgba(255,255,255,0.5); --dim: rgba(255,255,255,0.35);
+ --accent: #7c5cff; --accent2: #8a3dff; --accent-soft: rgba(124,92,255,0.14);
+ --radius: 10px;
+ }
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ [hidden] { display: none !important; }
+ body {
+ font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
+ background: var(--bg); color: var(--text); height: 100vh;
+ display: flex; flex-direction: column; overflow: hidden;
+ font-size: 13px; -webkit-font-smoothing: antialiased;
+ }
+ .topbar {
+ display: flex; align-items: center; gap: 16px; padding: 8px 16px;
+ background: var(--panel); border-bottom: 1px solid var(--border); flex-shrink: 0;
+ }
+ .brand { font-weight: 700; font-size: 0.92rem; white-space: nowrap; letter-spacing: -0.01em; }
+ .mode-tabs { display: flex; gap: 2px; background: var(--panel2); border: 1px solid var(--border); border-radius: 9px; padding: 3px; }
+ .mode-tabs button {
+ border: none; border-radius: 7px; padding: 6px 16px; font-weight: 550; font-size: 0.82rem;
+ background: transparent; color: var(--muted); cursor: pointer; transition: color 0.15s, background 0.15s;
+ }
+ .mode-tabs button:hover { color: var(--text); }
+ .mode-tabs button.active { background: var(--raised); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,0.45); }
+ .spacer { flex: 1; }
+ .ml-badge { color: var(--muted); text-decoration: none; font-size: 0.8rem; font-weight: 500; transition: color 0.15s; white-space: nowrap; }
+ .ml-badge:hover { color: var(--text); }
+ .ml-cta {
+ color: var(--accent); text-decoration: none; font-size: 0.8rem; font-weight: 600;
+ padding: 5px 13px; border: 1px solid var(--accent); border-radius: 999px;
+ background: var(--accent-soft); transition: filter 0.15s; white-space: nowrap;
+ }
+ .ml-cta:hover { filter: brightness(1.2); }
+ .main { display: flex; flex: 1; min-height: 0; }
+ .rail {
+ width: 52px; flex-shrink: 0; display: flex; flex-direction: column; align-items: center;
+ gap: 6px; padding: 10px 0; background: var(--panel); border-right: 1px solid var(--border);
+ }
+ .rail button {
+ width: 38px; height: 38px; border: 1px solid transparent; border-radius: 8px;
+ background: transparent; color: var(--muted); cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ transition: color 0.15s, background 0.15s;
+ }
+ .rail button:hover { background: var(--panel2); color: var(--text); }
+ .rail button.active { color: var(--accent); background: var(--accent-soft); }
+ .viewport { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: auto; position: relative; }
+ .workflow-view { position: absolute; inset: 0; padding: 12px; }
+ .workflow-view iframe { width: 100%; height: 100%; border: none; border-radius: 10px; background: #0c0d10; }
+ .doc-grid { flex: 1; min-height: 0; display: flex; gap: 16px; width: 100%; align-items: stretch; justify-content: center; padding: 20px 20px 10px; }
+ .doc {
+ flex: 1; min-width: 0; max-width: 640px; display: flex; flex-direction: column;
+ background: var(--panel); border: 1px solid var(--border); border-radius: 10px; overflow: hidden;
+ }
+ .doc figcaption {
+ padding: 8px 12px; font-size: 0.62rem; font-weight: 650; letter-spacing: 0.12em;
+ color: var(--dim); border-bottom: 1px solid var(--border); text-transform: uppercase;
+ display: flex; align-items: center;
+ }
+ .doc .stage {
+ position: relative; flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center;
+ overflow: hidden; background-color: #0b0b0d;
+ background-image:
+ linear-gradient(45deg, #141419 25%, transparent 25%),
+ linear-gradient(-45deg, #141419 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, #141419 75%),
+ linear-gradient(-45deg, transparent 75%, #141419 75%);
+ background-size: 20px 20px;
+ background-position: 0 0, 0 10px, 10px -10px, -10px 0;
+ }
+ .doc img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
+ .doc img.zoomed { max-width: none; max-height: none; }
+ .doc .stage.scrollable { overflow: auto; justify-content: flex-start; align-items: flex-start; }
+ #mask-canvas { position: absolute; inset: 0; cursor: crosshair; display: none; }
+ #mask-canvas.active { display: block; }
+ .drop-hint { color: var(--dim); font-size: 0.85rem; text-align: center; padding: 20px; }
+ .drop-hint b { color: var(--muted); display: block; margin-bottom: 6px; }
+ .zoom-btn {
+ margin-left: auto; border: 1px solid var(--border); background: transparent; color: var(--muted);
+ border-radius: 5px; font-size: 0.62rem; padding: 2px 8px; cursor: pointer; letter-spacing: 0.08em;
+ }
+ .zoom-btn:hover { color: var(--text); border-color: var(--accent); }
+ .doc-bar {
+ display: flex; align-items: center; gap: 10px; padding: 6px 12px;
+ border-top: 1px solid var(--border); background: var(--panel);
+ font-size: 0.72rem; color: var(--muted); flex-shrink: 0;
+ }
+ .doc-bar button {
+ border: 1px solid var(--border); background: transparent; color: var(--muted);
+ border-radius: 5px; padding: 3px 10px; font-size: 0.68rem; cursor: pointer; letter-spacing: 0.06em;
+ }
+ .doc-bar button:hover, .doc-bar button.active { color: var(--text); border-color: var(--accent); }
+ .doc-bar button.active { background: var(--accent-soft); color: var(--accent); }
+ .split-wrap { position: absolute; inset: 0; overflow: hidden; }
+ .split-wrap img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
+ .split-divider {
+ position: absolute; top: 0; bottom: 0; width: 3px; background: var(--accent);
+ cursor: ew-resize; z-index: 5; box-shadow: 0 0 8px rgba(124,92,255,0.6);
+ }
+ .split-divider::after {
+ content: "◂ ▸"; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
+ background: var(--accent); color: white; border-radius: 999px; padding: 4px 7px;
+ font-size: 0.6rem; white-space: nowrap;
+ }
+ .history-strip {
+ display: flex; gap: 8px; padding: 8px 12px; overflow-x: auto; flex-shrink: 0;
+ background: var(--panel); border-top: 1px solid var(--border); align-items: center;
+ }
+ .history-strip .hlabel { font-size: 0.62rem; font-weight: 650; letter-spacing: 0.14em; color: var(--dim); text-transform: uppercase; white-space: nowrap; }
+ .history-chip {
+ width: 52px; height: 52px; border-radius: 6px; border: 1px solid var(--border);
+ overflow: hidden; cursor: pointer; flex-shrink: 0; position: relative;
+ }
+ .history-chip:hover { border-color: var(--accent); }
+ .history-chip img { width: 100%; height: 100%; object-fit: cover; }
+ .history-chip .hseed {
+ position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.65);
+ color: #ddd; font-size: 0.52rem; padding: 1px 3px; text-align: center;
+ }
+ .panels {
+ width: 320px; flex-shrink: 0; display: flex; flex-direction: column; gap: 10px;
+ padding: 12px; overflow-y: auto; background: var(--panel); border-left: 1px solid var(--border);
+ }
+ .panel { background: var(--panel2); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; }
+ .panel-title { font-size: 0.62rem; font-weight: 650; letter-spacing: 0.12em; color: var(--dim); text-transform: uppercase; margin-bottom: 10px; cursor: pointer; user-select: none; display: flex; align-items: center; justify-content: space-between; }
+ .panel .panel-title::after { content: "▾"; font-size: 0.7rem; color: var(--dim); }
+ .panel.collapsed .panel-title::after { content: "▸"; }
+ .panel.collapsed .panel-body { display: none; }
+ .panel label { display: block; font-size: 0.72rem; font-weight: 550; color: var(--muted); margin: 10px 0 4px; }
+ .panel label:first-of-type { margin-top: 0; }
+ textarea, input[type="text"], input[type="number"] {
+ width: 100%; background: var(--raised); color: var(--text); border: 1px solid var(--border);
+ border-radius: 7px; padding: 8px 10px; font-family: inherit; font-size: 0.83rem; resize: vertical;
+ transition: border-color 0.15s, box-shadow 0.15s;
+ }
+ textarea:focus, input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
+ textarea.code { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 0.75rem; }
+ .row { display: flex; gap: 8px; align-items: center; }
+ .row > * { flex: 1; }
+ input[type="range"] {
+ -webkit-appearance: none; appearance: none; width: 100%; height: 3px;
+ border-radius: 2px; background: var(--raised); outline: none;
+ }
+ input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none; appearance: none; width: 14px; height: 14px; border-radius: 50%;
+ background: #fff; border: none; box-shadow: 0 1px 4px rgba(0,0,0,0.55); cursor: pointer;
+ transition: transform 0.12s;
+ }
+ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2); }
+ input[type="range"]::-moz-range-thumb {
+ width: 14px; height: 14px; border-radius: 50%; background: #fff; border: none;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.55); cursor: pointer;
+ }
+ .switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
+ .switch input { display: none; }
+ .switch .sw {
+ display: inline-block;
+ width: 34px; height: 20px; border-radius: 999px; background: var(--raised);
+ border: 1px solid var(--border); position: relative; flex-shrink: 0; transition: background 0.18s, border-color 0.18s;
+ }
+ .switch .sw::after {
+ content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
+ border-radius: 50%; background: var(--muted); transition: transform 0.18s, background 0.18s;
+ }
+ .switch input:checked + .sw { background: var(--accent); border-color: var(--accent); }
+ .switch input:checked + .sw::after { transform: translateX(14px); background: #fff; }
+ .switch .sw-label { font-size: 0.75rem; color: var(--muted); }
+ .layer-row {
+ display: flex; align-items: center; gap: 10px; padding: 6px 8px;
+ border: 1px solid var(--border); border-radius: 6px; margin-top: 6px;
+ background: var(--raised); cursor: pointer;
+ }
+ .layer-row:hover { border-color: var(--accent); }
+ .layer-row .thumb {
+ width: 40px; height: 40px; border-radius: 4px; object-fit: cover; flex-shrink: 0;
+ background: #101014; border: 1px solid var(--border);
+ display: flex; align-items: center; justify-content: center;
+ color: var(--dim); font-size: 0.65rem; text-align: center;
+ }
+ .layer-row .thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 3px; }
+ .layer-row .lname { flex: 1; font-size: 0.78rem; color: var(--text); }
+ .layer-row .lsub { font-size: 0.62rem; color: var(--dim); display: block; }
+ .layer-row .ldel {
+ border: none; background: transparent; color: var(--dim); cursor: pointer;
+ font-size: 0.9rem; padding: 2px 6px; border-radius: 4px;
+ }
+ .layer-row .ldel:hover { color: #ff6f91; background: rgba(255,111,145,0.1); }
+ .layer-row.mask-layer .thumb { border-color: var(--accent); }
+ .ref-slot {
+ height: 72px; border: 1px dashed var(--border); border-radius: 6px; margin-top: 8px;
+ display: flex; align-items: center; justify-content: center; cursor: pointer; overflow: hidden;
+ color: var(--dim); font-size: 0.72rem; background: var(--raised);
+ }
+ .ref-slot:hover { border-color: var(--accent); }
+ .ref-slot img { width: 100%; height: 100%; object-fit: cover; }
+ .hint { font-size: 0.68rem; color: var(--dim); margin-top: 8px; line-height: 1.4; }
+ .ex-row { display: flex; gap: 8px; overflow-x: auto; }
+ .ex-chip {
+ flex: 1; min-width: 76px; border: 1px solid var(--border); border-radius: 8px;
+ overflow: hidden; cursor: pointer; background: var(--raised); padding: 0;
+ text-align: left; transition: border-color 0.15s;
+ }
+ .ex-chip:hover { border-color: var(--accent); }
+ .ex-chip img { width: 100%; height: 64px; object-fit: cover; display: block; }
+ .ex-chip span { display: block; padding: 5px 7px; font-size: 0.62rem; color: var(--muted); line-height: 1.25; }
+ #run-btn {
+ margin-top: 4px; padding: 12px; border: none; border-radius: 8px; cursor: pointer;
+ background: var(--accent); color: white;
+ font-weight: 600; font-size: 0.9rem; letter-spacing: 0.01em;
+ transition: filter 0.15s, transform 0.05s;
+ }
+ #run-btn:hover { filter: brightness(1.12); }
+ #run-btn:active { transform: translateY(1px); }
+ #run-btn:disabled { opacity: 0.5; cursor: wait; }
+ .statusbar {
+ display: flex; align-items: center; gap: 16px; padding: 7px 16px; flex-shrink: 0;
+ background: var(--panel); border-top: 1px solid var(--border);
+ font-size: 0.75rem; color: var(--muted);
+ }
+ .statusbar .pulse { width: 8px; height: 8px; border-radius: 50%; background: #4fd1a5; }
+ .statusbar.busy .pulse { background: var(--accent); animation: pulse 1s infinite alternate; }
+ @keyframes pulse { from { opacity: 0.4; } to { opacity: 1; } }
+ #file-input { display: none; }
+ @media (max-width: 860px) {
+ html, body { height: auto !important; overflow: visible !important; }
+ body { display: block; }
+ .topbar { flex-wrap: wrap; gap: 8px; padding: 8px 12px; }
+ .brand { font-size: 0.88rem; }
+ .topbar .ml-badge { display: none; }
+ .main { display: block; height: auto; min-height: 0; }
+ .rail {
+ flex-direction: row; width: 100%; justify-content: center; gap: 10px;
+ padding: 6px 10px; border-right: none; border-bottom: 1px solid var(--border);
+ }
+ .rail button { width: 44px; height: 44px; }
+ .viewport { display: block; padding: 10px; overflow: visible; }
+ .workflow-view { position: static; height: 78vh; padding: 0 0 10px; }
+ .doc-grid { flex-direction: column; align-items: stretch; height: auto; }
+ .doc { max-width: 100%; width: 100%; }
+ .doc .stage { min-height: 220px; }
+ .panels {
+ width: 100%; border-left: none; border-top: 1px solid var(--border);
+ padding: 10px 12px; overflow-y: visible;
+ }
+ .mode-tabs button { padding: 6px 13px; font-size: 0.8rem; }
+ .statusbar { position: sticky; bottom: 0; }
+ }
+</style>
+</head>
+<body>
+ <header class="topbar">
+ <div class="brand">⚡ FIBO 2.0 Edit Turbo</div>
+ <div class="mode-tabs">
+ <button id="mode-edit" class="active">Edit</button>
+ <button id="mode-workflow">Workflow</button>
+ </div>
+ <div class="spacer"></div>
+ <a class="ml-badge" href="https://huggingface.co/briaai/Fibo-2.0-edit-turbo" target="_blank" rel="noopener">Model</a>
+ <a class="ml-cta" href="/classic">Classic UI →</a>
+ </header>
+
+ <div class="main">
+ <div class="rail">
+ <button id="tool-upload" title="Upload source image"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></button>
+ <button id="tool-brush" title="Brush mask (single reference)"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9.06 11.9l8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.07"/><path d="M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z"/></svg></button>
+ <button id="tool-clear-mask" title="Clear mask"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20H8L3 15a2 2 0 0 1 0-2.83l7.17-7.17a2 2 0 0 1 2.83 0l5.66 5.66a2 2 0 0 1 0 2.83L13 19"/><line x1="18" y1="11" x2="9" y2="20"/></svg></button>
+ <button id="tool-clear-doc" title="Clear document"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
+ </div>
+
+ <div class="viewport">
+ <div class="workflow-view" id="workflow-view" hidden>
+ <iframe src="/workflow/?ui=canvas" title="Workflow canvas"></iframe>
+ </div>
+ <div class="doc-grid">
+ <figure class="doc" id="doc-before">
+ <figcaption id="before-caption">Source</figcaption>
+ <div class="stage" id="before-stage">
+ <img id="source-img" alt="" hidden />
+ <canvas id="mask-canvas"></canvas>
+ <div class="drop-hint" id="source-hint"><b>No source image</b>Use the upload tool or drop a file here</div>
+ </div>
+ </figure>
+ <figure class="doc" id="doc-after">
+ <figcaption id="after-caption">Result</figcaption>
+ <div class="stage" id="after-stage">
+ <img id="result-img" alt="" hidden />
+ <div class="split-wrap" id="split-wrap" hidden>
+ <img id="split-source" alt="" style="clip-path: inset(0 50% 0 0)" />
+ <div class="split-divider" id="split-divider" style="left:50%"></div>
+ </div>
+ <div class="drop-hint" id="result-hint"><b>Nothing yet</b>Run to see the result</div>
+ </div>
+ <div class="doc-bar">
+ <span id="result-meta">—</span>
+ <div class="spacer"></div>
+ <button id="split-toggle" hidden>SPLIT</button>
+ </div>
+ </figure>
+ </div>
+ <div class="history-strip" id="history-strip"><span class="hlabel">History</span></div>
+ </div>
+
+ <aside class="panels">
+ <section class="panel">
+ <div class="panel-title">Examples</div>
+ <div class="ex-row">
+ <button class="ex-chip" data-img="/examples/cat_tabby.jpg" data-instruction="Put a small party hat on the cat">
+ <img src="/examples/cat_tabby.jpg" alt="" /><span>Party hat on the cat</span>
+ </button>
+ <button class="ex-chip" data-img="/examples/bicycle_wall.jpg" data-instruction="Change the bicycle color to bright red">
+ <img src="/examples/bicycle_wall.jpg" alt="" /><span>Red bicycle</span>
+ </button>
+ <button class="ex-chip" data-img="/examples/businessman_suit.jpg" data-instruction="Add round glasses">
+ <img src="/examples/businessman_suit.jpg" alt="" /><span>Round glasses</span>
+ </button>
+ </div>
+ </section>
+ <section class="panel">
+ <div class="panel-title">Instruction</div>
+ <label for="instruction">What should change?</label>
+ <textarea id="instruction" rows="3" placeholder="make the sky dramatic and stormy"></textarea>
+ <label for="json-instruction">JSON override (optional; auto-filled by the VGL prompt writer)</label>
+ <textarea id="json-instruction" class="code" rows="5" placeholder='{"edit_instruction": "…"}'></textarea>
+ </section>
+ <section class="panel">
+ <div class="panel-title">Layers</div>
+ <div class="layer-row" id="layer-source">
+ <div class="thumb" id="layer-source-thumb">BG</div>
+ <div class="lname">Background<span class="lsub">source image</span></div>
+ </div>
+ <div class="layer-row mask-layer" id="layer-mask" hidden>
+ <div class="thumb" id="layer-mask-thumb"></div>
+ <div class="lname">Mask<span class="lsub">repaint region</span></div>
+ <button class="ldel" id="layer-mask-del" title="Clear mask">✕</button>
+ </div>
+ <div class="ref-slot" data-ref="2">+ Reference 2</div>
+ <div class="ref-slot" data-ref="3">+ Reference 3</div>
+ <div class="ref-slot" data-ref="4">+ Reference 4</div>
+ <div class="hint">Slots map to &lt;image_1&gt;…&lt;image_4&gt; in the instruction. A mask only works with a single reference.</div>
+ </section>
+ <section class="panel">
+ <div class="panel-title">Advanced</div>
+ <div class="row">
+ <div><label for="seed">Seed</label><input type="number" id="seed" value="42" min="1" /></div>
+ <label class="switch"><input type="checkbox" id="randomize" checked /><span class="sw"></span><span class="sw-label">Randomize</span></label>
+ </div>
+ <div class="row" style="margin-top:10px">
+ <div><label>Steps <span id="steps-val">4</span></label><input type="range" id="steps" min="1" max="8" value="4" /></div>
+ </div>
+ <div class="hint">Edit Turbo is distilled for 4 guidance-free steps.</div>
+ </section>
+ <button id="run-btn">Generate Edit</button>
+ </aside>
+ </div>
+
+ <footer class="statusbar" id="statusbar">
+ <div class="pulse"></div>
+ <span id="status">Idle</span>
+ <div class="spacer"></div>
+ <span id="seed-out"></span>
+ </footer>
+
+ <input type="file" id="file-input" accept="image/*" />
+
+<script type="module">
+ import { client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
+
+ const $ = (id) => document.getElementById(id);
+ const state = { mode: "edit", sourcePath: null, refPaths: { 2: null, 3: null, 4: null }, app: null };
+ const docState = { source: null, result: null, history: [] };
+
+ async function getApp() {
+ if (!state.app) state.app = await client(location.origin);
+ return state.app;
+ }
+
+ async function uploadFile(file) {
+ const form = new FormData();
+ form.append("files", file);
+ const resp = await fetch("/gradio_api/upload", { method: "POST", body: form });
+ if (!resp.ok) throw new Error(`Upload failed (${resp.status})`);
+ const paths = await resp.json();
+ return Array.isArray(paths) ? paths[0] : paths;
+ }
+
+ function setStatus(text, busy = false) {
+ $("status").textContent = text;
+ $("statusbar").classList.toggle("busy", busy);
+ $("run-btn").disabled = busy;
+ }
+
+ function setMode(mode) {
+ state.mode = mode;
+ $("mode-edit").classList.toggle("active", mode === "edit");
+ $("mode-workflow").classList.toggle("active", mode === "workflow");
+ const workflow = mode === "workflow";
+ $("workflow-view").hidden = !workflow;
+ document.querySelector(".doc-grid").style.display = workflow ? "none" : "";
+ document.querySelector(".rail").style.display = workflow ? "none" : "";
+ document.querySelector(".panels").style.display = workflow ? "none" : "";
+ document.getElementById("history-strip").style.display = workflow ? "none" : "";
+ }
+ $("mode-edit").onclick = () => setMode("edit");
+ $("mode-workflow").onclick = () => setMode("workflow");
+
+ // ---------- uploads ----------
+ let uploadTarget = "source";
+ function pickFile(target) {
+ uploadTarget = target;
+ $("file-input").click();
+ }
+ $("tool-upload").onclick = () => pickFile("source");
+ document.querySelectorAll(".ref-slot").forEach((slot) => {
+ slot.onclick = () => pickFile(slot.dataset.ref);
+ });
+ $("file-input").onchange = async (e) => {
+ const file = e.target.files[0];
+ e.target.value = "";
+ if (!file) return;
+ setStatus("Uploading…", true);
+ try {
+ const path = await uploadFile(file);
+ if (uploadTarget === "source") {
+ state.sourcePath = path;
+ showSource(file);
+ } else {
+ state.refPaths[uploadTarget] = path;
+ const slot = document.querySelector(`.ref-slot[data-ref="${uploadTarget}"]`);
+ slot.innerHTML = "";
+ const img = document.createElement("img");
+ img.src = URL.createObjectURL(file);
+ slot.appendChild(img);
+ }
+ setStatus("Idle");
+ } catch (err) {
+ setStatus(String(err));
+ }
+ };
+ $("before-stage").addEventListener("dragover", (e) => e.preventDefault());
+ $("before-stage").addEventListener("drop", async (e) => {
+ e.preventDefault();
+ const file = e.dataTransfer.files[0];
+ if (!file) return;
+ setStatus("Uploading…", true);
+ try {
+ state.sourcePath = await uploadFile(file);
+ showSource(file);
+ setStatus("Idle");
+ } catch (err) {
+ setStatus(String(err));
+ }
+ });
+
+ function showSource(file) {
+ const img = $("source-img");
+ img.src = URL.createObjectURL(file);
+ img.hidden = false;
+ $("source-hint").style.display = "none";
+ img.onload = () => sizeMaskCanvas();
+ clearMask();
+ docState.source = img.src;
+ const st = $("layer-source-thumb");
+ st.innerHTML = "";
+ const stImg = document.createElement("img");
+ stImg.src = img.src;
+ st.appendChild(stImg);
+ }
+
+ // ---------- mask brush ----------
+ const maskCanvas = $("mask-canvas");
+ const mctx = maskCanvas.getContext("2d");
+ let strokes = [];
+ let drawing = false;
+ function sizeMaskCanvas() {
+ const stage = $("before-stage");
+ const img = $("source-img");
+ if (!img.src || img.hidden) return;
+ const box = img.getBoundingClientRect();
+ const stageBox = stage.getBoundingClientRect();
+ maskCanvas.width = box.width;
+ maskCanvas.height = box.height;
+ maskCanvas.style.left = box.left - stageBox.left + "px";
+ maskCanvas.style.top = box.top - stageBox.top + "px";
+ maskCanvas.style.width = box.width + "px";
+ maskCanvas.style.height = box.height + "px";
+ mctx.fillStyle = "#000";
+ mctx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
+ }
+ function brushPos(e) {
+ const box = maskCanvas.getBoundingClientRect();
+ return { x: e.clientX - box.left, y: e.clientY - box.top };
+ }
+ maskCanvas.addEventListener("pointerdown", (e) => { drawing = true; strokes.push([brushPos(e)]); });
+ maskCanvas.addEventListener("pointermove", (e) => {
+ if (!drawing) return;
+ strokes[strokes.length - 1].push(brushPos(e));
+ redrawMask();
+ });
+ window.addEventListener("pointerup", () => {
+ drawing = false;
+ if (state.brushOn && strokes.length) {
+ $("layer-mask").hidden = false;
+ const mt = $("layer-mask-thumb");
+ mt.innerHTML = "";
+ const t = document.createElement("img");
+ t.src = maskCanvas.toDataURL();
+ mt.appendChild(t);
+ }
+ });
+ function redrawMask() {
+ mctx.fillStyle = "#000";
+ mctx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
+ mctx.strokeStyle = "#fff";
+ mctx.lineWidth = Math.max(16, maskCanvas.width / 40);
+ mctx.lineCap = "round";
+ mctx.lineJoin = "round";
+ for (const stroke of strokes) {
+ mctx.beginPath();
+ stroke.forEach((p, i) => (i ? mctx.lineTo(p.x, p.y) : mctx.moveTo(p.x, p.y)));
+ mctx.stroke();
+ }
+ mctx.save();
+ mctx.globalCompositeOperation = "source-atop";
+ mctx.fillStyle = "rgba(124,92,255,0.35)";
+ mctx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
+ mctx.restore();
+ }
+ function clearMask() {
+ strokes = [];
+ maskCanvas.classList.remove("active");
+ $("tool-brush").classList.remove("active");
+ mctx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
+ $("layer-mask").hidden = true;
+ }
+ state.brushOn = false;
+ $("tool-brush").onclick = () => {
+ if (state.sourcePath == null) { setStatus("Upload a source image first."); return; }
+ state.brushOn = !state.brushOn;
+ maskCanvas.classList.toggle("active", state.brushOn);
+ $("tool-brush").classList.toggle("active", state.brushOn);
+ if (state.brushOn) { sizeMaskCanvas(); redrawMask(); setStatus("Brush the area to repaint."); }
+ else setStatus("Idle");
+ };
+ $("tool-clear-mask").onclick = clearMask;
+ $("layer-mask-del").onclick = (e) => { e.stopPropagation(); clearMask(); };
+ $("tool-clear-doc").onclick = () => {
+ state.sourcePath = null;
+ docState.source = null;
+ docState.result = null;
+ $("source-img").hidden = true;
+ $("source-hint").style.display = "";
+ $("result-img").hidden = true;
+ $("result-hint").style.display = "";
+ $("layer-source-thumb").textContent = "BG";
+ clearMask();
+ };
+
+ async function maskPath() {
+ if (!strokes.length) return null;
+ const img = $("source-img");
+ const out = document.createElement("canvas");
+ out.width = img.naturalWidth;
+ out.height = img.naturalHeight;
+ const octx = out.getContext("2d");
+ octx.fillStyle = "#000";
+ octx.fillRect(0, 0, out.width, out.height);
+ const sx = out.width / maskCanvas.width;
+ const sy = out.height / maskCanvas.height;
+ octx.strokeStyle = "#fff";
+ octx.lineWidth = Math.max(16, maskCanvas.width / 40) * sx;
+ octx.lineCap = "round";
+ octx.lineJoin = "round";
+ for (const stroke of strokes) {
+ octx.beginPath();
+ stroke.forEach((p, i) => (i ? octx.lineTo(p.x * sx, p.y * sy) : octx.moveTo(p.x * sx, p.y * sy)));
+ octx.stroke();
+ }
+ const blob = await new Promise((r) => out.toBlob(r, "image/png"));
+ return uploadFile(blob);
+ }
+
+ // ---------- history ----------
+ function renderHistory() {
+ const strip = $("history-strip");
+ strip.innerHTML = '<span class="hlabel">History</span>';
+ docState.history.forEach((h) => {
+ const chip = document.createElement("div");
+ chip.className = "history-chip";
+ chip.title = h.instruction || "";
+ const img = document.createElement("img");
+ img.src = h.uri;
+ const tag = document.createElement("div");
+ tag.className = "hseed";
+ tag.textContent = h.seed ?? "";
+ chip.appendChild(img);
+ chip.appendChild(tag);
+ chip.onclick = () => {
+ $("instruction").value = h.instruction;
+ $("seed").value = h.seed ?? 42;
+ $("randomize").checked = false;
+ docState.result = h.uri;
+ $("result-img").src = h.uri;
+ $("result-img").hidden = false;
+ $("result-hint").style.display = "none";
+ $("split-toggle").hidden = false;
+ };
+ strip.appendChild(chip);
+ });
+ }
+ function pushHistory(uri, seed, instruction) {
+ docState.history.unshift({ uri, seed, instruction });
+ if (docState.history.length > 8) docState.history.pop();
+ renderHistory();
+ }
+
+ // ---------- before/after split view ----------
+ let splitOn = false;
+ $("split-toggle").onclick = () => {
+ if ($("result-img").hidden) return;
+ splitOn = !splitOn;
+ $("split-toggle").classList.toggle("active", splitOn);
+ $("split-wrap").hidden = !splitOn;
+ if (splitOn && !$("source-img").hidden) $("split-source").src = $("source-img").src;
+ };
+ (function () {
+ const divider = $("split-divider");
+ let dragging = false;
+ divider.addEventListener("pointerdown", (e) => { dragging = true; e.preventDefault(); });
+ window.addEventListener("pointermove", (e) => {
+ if (!dragging) return;
+ const stage = $("after-stage").getBoundingClientRect();
+ const pct = Math.min(97, Math.max(3, ((e.clientX - stage.left) / stage.width) * 100));
+ divider.style.left = pct + "%";
+ $("split-source").style.clipPath = `inset(0 ${100 - pct}% 0 0)`;
+ });
+ window.addEventListener("pointerup", () => { dragging = false; });
+ })();
+
+ // ---------- zoom ----------
+ document.querySelectorAll(".doc figcaption").forEach((caption) => {
+ const btn = document.createElement("button");
+ btn.className = "zoom-btn";
+ btn.textContent = "FIT";
+ btn.onclick = () => {
+ const img = caption.parentElement.querySelector("img");
+ const stage = caption.parentElement.querySelector(".stage");
+ const zoomed = img.classList.toggle("zoomed");
+ stage.classList.toggle("scrollable", zoomed);
+ btn.textContent = zoomed ? "1:1" : "FIT";
+ };
+ caption.appendChild(btn);
+ });
+
+ // ---------- collapsible panels ----------
+ document.querySelectorAll(".panel .panel-title").forEach((title) => {
+ const body = document.createElement("div");
+ body.className = "panel-body";
+ [...title.parentElement.children].forEach((child) => {
+ if (child !== title) body.appendChild(child);
+ });
+ title.parentElement.appendChild(body);
+ title.onclick = () => title.parentElement.classList.toggle("collapsed");
+ });
+
+ // ---------- examples ----------
+ document.querySelectorAll(".ex-chip").forEach((chip) => {
+ chip.onclick = async () => {
+ setStatus("Loading example…", true);
+ try {
+ const blob = await (await fetch(chip.dataset.img)).blob();
+ const file = new File([blob], "example.jpg", { type: "image/jpeg" });
+ state.sourcePath = await uploadFile(file);
+ showSource(file);
+ $("instruction").value = chip.dataset.instruction;
+ setMode("edit");
+ setStatus("Example loaded — hit Generate Edit");
+ } catch (err) {
+ setStatus(String(err));
+ }
+ };
+ });
+
+ // ---------- result handling ----------
+ function unwrap(result) {
+ const data = Array.isArray(result?.data) ? result.data[0] : result?.data;
+ return data ?? {};
+ }
+ function showResult(result) {
+ const payload = unwrap(result);
+ const url = typeof payload.image_data === "string" ? payload.image_data : null;
+ if (!url) { setStatus("Run finished but no image came back (see console)."); return false; }
+ const img = $("result-img");
+ img.src = url;
+ img.hidden = false;
+ $("result-hint").style.display = "none";
+ docState.result = url;
+ if (payload.seed != null) $("seed-out").textContent = "seed " + payload.seed;
+ $("split-toggle").hidden = false;
+ $("result-meta").textContent = "Edited";
+ const instruction = ($("json-instruction").value.trim() || $("instruction").value);
+ pushHistory(url, payload.seed, instruction);
+ return true;
+ }
+
+ $("steps").oninput = () => ($("steps-val").textContent = $("steps").value);
+
+ $("run-btn").onclick = async () => {
+ const app = await getApp();
+ if (!state.sourcePath) { setStatus("Upload a source image first."); return; }
+ const seed = parseInt($("seed").value, 10) || 42;
+ const randomize = $("randomize").checked;
+ const steps = parseInt($("steps").value, 10);
+ try {
+ setStatus("Preparing mask…", true);
+ const mask = await maskPath();
+ setStatus($("json-instruction").value.trim() ? "Editing…" : "Writing the instruction (VGL)…", true);
+ const result = await app.predict("/edit", {
+ source: state.sourcePath,
+ instruction: $("instruction").value,
+ json_instruction: $("json-instruction").value,
+ ref2: state.refPaths[2],
+ ref3: state.refPaths[3],
+ ref4: state.refPaths[4],
+ mask,
+ seed, randomize, steps,
+ });
+ const payload = unwrap(result);
+ if (payload.json_instruction) $("json-instruction").value = payload.json_instruction;
+ if (showResult({ data: [payload] })) setStatus("Done");
+ } catch (err) {
+ setStatus(err?.message || String(err));
+ }
+ };
+</script>
+</body>
+</html>
diff --git a/workflow.json b/workflow.json
new file mode 100644
index 0000000..3a13639
--- /dev/null
+++ b/workflow.json
@@ -0,0 +1,155 @@
+{
+ "schema_version": "2",
+ "name": "FIBO 2.0 Edit Turbo Workflow",
+ "references": [
+ {
+ "id": "ref_source",
+ "label": "Source Image",
+ "role": "reference",
+ "asset_type": "image",
+ "inputs": [{"id": "in", "label": "Image", "type": "image"}],
+ "outputs": [{"id": "out", "label": "Image", "type": "image"}],
+ "x": 40,
+ "y": 40,
+ "width": 220,
+ "height": 130,
+ "data": {}
+ },
+ {
+ "id": "ref_instruction",
+ "label": "Edit Instruction (JSON)",
+ "role": "reference",
+ "asset_type": "text",
+ "inputs": [{"id": "in", "label": "Text", "type": "text"}],
+ "outputs": [{"id": "out", "label": "Text", "type": "text"}],
+ "x": 40,
+ "y": 200,
+ "width": 280,
+ "height": 140,
+ "data": {
+ "out": "{\n \"edit_instruction\": \"make the sky dramatic and stormy\"\n}"
+ }
+ },
+ {
+ "id": "ref_seed",
+ "label": "Seed",
+ "role": "reference",
+ "asset_type": "text",
+ "inputs": [{"id": "in", "label": "Text", "type": "text"}],
+ "outputs": [{"id": "out", "label": "Text", "type": "text"}],
+ "x": 340,
+ "y": 200,
+ "width": 130,
+ "height": 90,
+ "data": {"out": "42"}
+ },
+ {
+ "id": "ref_steps",
+ "label": "Steps",
+ "role": "reference",
+ "asset_type": "text",
+ "inputs": [{"id": "in", "label": "Text", "type": "text"}],
+ "outputs": [{"id": "out", "label": "Text", "type": "text"}],
+ "x": 340,
+ "y": 310,
+ "width": 130,
+ "height": 90,
+ "data": {"out": "4"}
+ },
+ {
+ "id": "ref_instruction_2",
+ "label": "Second Instruction (JSON)",
+ "role": "reference",
+ "asset_type": "text",
+ "inputs": [{"id": "in", "label": "Text", "type": "text"}],
+ "outputs": [{"id": "out", "label": "Text", "type": "text"}],
+ "x": 640,
+ "y": 40,
+ "width": 280,
+ "height": 130,
+ "data": {
+ "out": "{\n \"edit_instruction\": \"turn the scene into night with warm window lights\"\n}"
+ }
+ }
+ ],
+ "operators": [
+ {
+ "id": "op_edit",
+ "label": "FIBO Edit",
+ "role": "operator",
+ "kind": "fn",
+ "fn": "fibo_edit",
+ "inputs": [
+ {"id": "image", "label": "Image", "type": "image", "required": true},
+ {"id": "json_instruction", "label": "JSON Instruction", "type": "text", "required": true},
+ {"id": "seed", "label": "Seed", "type": "text"},
+ {"id": "steps", "label": "Steps", "type": "text"}
+ ],
+ "outputs": [{"id": "out_0", "label": "Image", "type": "image", "output_index": 0}],
+ "x": 520,
+ "y": 120,
+ "width": 230,
+ "height": 160,
+ "data": {}
+ },
+ {
+ "id": "op_edit_2",
+ "label": "FIBO Edit (chained)",
+ "role": "operator",
+ "kind": "fn",
+ "fn": "fibo_edit",
+ "inputs": [
+ {"id": "image", "label": "Image", "type": "image", "required": true},
+ {"id": "json_instruction", "label": "JSON Instruction", "type": "text", "required": true},
+ {"id": "seed", "label": "Seed", "type": "text"},
+ {"id": "steps", "label": "Steps", "type": "text"}
+ ],
+ "outputs": [{"id": "out_0", "label": "Image", "type": "image", "output_index": 0}],
+ "x": 960,
+ "y": 120,
+ "width": 230,
+ "height": 160,
+ "data": {}
+ }
+ ],
+ "subjects": [
+ {
+ "id": "sub_edited",
+ "label": "Edited Image",
+ "role": "subject",
+ "asset_type": "image",
+ "inputs": [{"id": "in", "label": "Image", "type": "image"}],
+ "outputs": [{"id": "out", "label": "Image", "type": "image"}],
+ "x": 780,
+ "y": 340,
+ "width": 230,
+ "height": 110,
+ "data": {}
+ },
+ {
+ "id": "sub_chained",
+ "label": "Final Image",
+ "role": "subject",
+ "asset_type": "image",
+ "inputs": [{"id": "in", "label": "Image", "type": "image"}],
+ "outputs": [{"id": "out", "label": "Image", "type": "image"}],
+ "x": 1240,
+ "y": 120,
+ "width": 230,
+ "height": 110,
+ "data": {}
+ }
+ ],
+ "edges": [
+ {"id": "e_source", "from_node_id": "ref_source", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "image", "type": "image"},
+ {"id": "e_instruction", "from_node_id": "ref_instruction", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "json_instruction", "type": "text"},
+ {"id": "e_seed", "from_node_id": "ref_seed", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "seed", "type": "text"},
+ {"id": "e_steps", "from_node_id": "ref_steps", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "steps", "type": "text"},
+ {"id": "e_out", "from_node_id": "op_edit", "from_port_id": "out_0", "to_node_id": "sub_edited", "to_port_id": "in", "type": "image"},
+ {"id": "e_chain", "from_node_id": "op_edit", "from_port_id": "out_0", "to_node_id": "op_edit_2", "to_port_id": "image", "type": "image"},
+ {"id": "e_instruction_2", "from_node_id": "ref_instruction_2", "from_port_id": "out", "to_node_id": "op_edit_2", "to_port_id": "json_instruction", "type": "text"},
+ {"id": "e_seed_2", "from_node_id": "ref_seed", "from_port_id": "out", "to_node_id": "op_edit_2", "to_port_id": "seed", "type": "text"},
+ {"id": "e_steps_2", "from_node_id": "ref_steps", "from_port_id": "out", "to_node_id": "op_edit_2", "to_port_id": "steps", "type": "text"},
+ {"id": "e_out_2", "from_node_id": "op_edit_2", "from_port_id": "out_0", "to_node_id": "sub_chained", "to_port_id": "in", "type": "image"}
+ ]
+}
--
2.47.3