From 4a7d045978112eaf211d0bac75662a81be096471 Mon Sep 17 00:00:00 2001 From: akhaliq 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 .. 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 ( + '' + "
⚡
" + f"" + 'Sign in with Hugging Face →' + "

Opens in a new tab — this page stays here.

" + f"" + 'Signed in? Back to the canvas ↻' + '' + ) -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""" -
- BRIA logo -
-

FIBO 2.0 Edit Turbo

-

8B image editing in 4 steps, driven by structured JSON instructions — with references and masks.

- Open model · 8B · Edit Turbo -
-
- """) - - 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 " - "`` … ``." - ) - 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(""" - - """) - -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 .. 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""" +
+ BRIA logo +
+

FIBO 2.0 Edit Turbo

+

8B image editing in 4 steps, driven by structured JSON instructions — with references and masks.

+ Open model · 8B · Edit Turbo +
+
+ """) + + 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 " + "`` … ``." + ) + 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(""" + + """) + diff --git a/studio.html b/studio.html new file mode 100644 index 0000000..892c465 --- /dev/null +++ b/studio.html @@ -0,0 +1,746 @@ + + + + + +FIBO 2.0 Edit Turbo Studio + + + +
+
⚡ FIBO 2.0 Edit Turbo
+
+ + +
+
+ Model + Classic UI → +
+ +
+
+ + + + +
+ +
+ +
+
+
Source
+
+ + +
No source imageUse the upload tool or drop a file here
+
+
+
+
Result
+
+ + +
Nothing yetRun to see the result
+
+
+ — +
+ +
+
+
+
History
+
+ + +
+ +
+
+ Idle +
+ +
+ + + + + + 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