Pierre Chapuis commited on
Commit
801ec70
·
unverified ·
1 Parent(s): df285d1

initial commit

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.png filter=lfs diff=lfs merge=lfs -text
37
+ *.jpg filter=lfs diff=lfs merge=lfs -text
38
+ *.webp filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,11 +1,12 @@
1
  ---
2
- title: Finegrain Product Placement Lora
3
- emoji: 🦀
4
- colorFrom: pink
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 5.45.0
8
- app_file: app.py
 
9
  pinned: false
10
  license: mit
11
  short_description: Flux Kontext extended with product placement capabilities
 
1
  ---
2
+ title: Finegrain Product Placement LoRA
3
+ emoji: 📚
4
+ colorFrom: blue
5
+ colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 5.45.0
8
+ python_version: 3.10
9
+ app_file: src/app.py
10
  pinned: false
11
  license: mit
12
  short_description: Flux Kontext extended with product placement capabilities
examples/chair/reference.webp ADDED

Git LFS Details

  • SHA256: 40235634c400e23373a2996a12f060f01e5b0396182fa90128acde0d4af5eec4
  • Pointer size: 130 Bytes
  • Size of remote file: 12.2 kB
examples/chair/scene.webp ADDED

Git LFS Details

  • SHA256: 901f339f046bacb7cc18346f399e0362761c771832d7e02bf1c7ebe4d4735edf
  • Pointer size: 130 Bytes
  • Size of remote file: 29.8 kB
examples/glass/reference.webp ADDED

Git LFS Details

  • SHA256: bba62e68587b4cb4b9248d787322980f494b79bf1cd19b846f28c064079cdbe1
  • Pointer size: 130 Bytes
  • Size of remote file: 33.6 kB
examples/glass/scene.webp ADDED

Git LFS Details

  • SHA256: 391cbab4c4c5eb01b7be7d5ef14bdbbfba1642c89cfd7812af447618f42f50d5
  • Pointer size: 131 Bytes
  • Size of remote file: 374 kB
examples/kitchen/reference.webp ADDED

Git LFS Details

  • SHA256: a820a749a786380f345a94ad0c4ac7e12bd05337a2b971890b72772ae92f615a
  • Pointer size: 130 Bytes
  • Size of remote file: 15.1 kB
examples/kitchen/scene.webp ADDED

Git LFS Details

  • SHA256: 8125dade00faceb3c3807befc59e30ae5dda123cb76ffbecf00d076970887d02
  • Pointer size: 131 Bytes
  • Size of remote file: 258 kB
examples/lantern/reference.webp ADDED

Git LFS Details

  • SHA256: 5bc2dfc09a549280f17d8a8ab9db2ddb5bc76782afeea9bb9a1d4f89cf91fe46
  • Pointer size: 131 Bytes
  • Size of remote file: 117 kB
examples/lantern/scene.webp ADDED

Git LFS Details

  • SHA256: f853399158c27932dac7da907e06e69cbee21da9363d33541446f6e34b984afe
  • Pointer size: 131 Bytes
  • Size of remote file: 205 kB
examples/sunglasses/reference.webp ADDED

Git LFS Details

  • SHA256: 184fe3600d42f70041c59f59dfebccbc0a5703955f40ae1cce11d416f820abaa
  • Pointer size: 130 Bytes
  • Size of remote file: 89.7 kB
examples/sunglasses/scene.jpg ADDED

Git LFS Details

  • SHA256: b02d7c89c2281eaf459226176c8e94be8c463795d29aa2383802b111166243e3
  • Pointer size: 130 Bytes
  • Size of remote file: 57.9 kB
pyproject.toml ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "finegrain-product-placement-lora"
3
+ version = "0.1.0"
4
+ description = "Flux Kontext extended with product placement capabilities"
5
+ authors = [
6
+ { name = "Cédric Deltheil", email = "cedric@lagon.tech" },
7
+ { name = "Pierre Chapuis", email = "pierre@lagon.tech" },
8
+ ]
9
+ dependencies = [
10
+ "gradio>=5.35.0",
11
+ "spaces>=0.37.1",
12
+ "pillow>=11.3.0",
13
+ "gradio-image-annotation>=0.4.0",
14
+ "finegrain_toolbox @ git+ssh://git@github.com/finegrain-ai/finegrain-toolbox",
15
+ "finegrain @ git+https://github.com/finegrain-ai/finegrain-python@py310#subdirectory=finegrain",
16
+ ]
17
+ readme = "README.md"
18
+ requires-python = ">= 3.10"
19
+ classifiers = ["Private :: Do Not Upload"]
20
+
21
+ [build-system]
22
+ requires = ["hatchling"]
23
+ build-backend = "hatchling.build"
24
+
25
+ [tool.hatch.metadata]
26
+ allow-direct-references = true
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src"]
30
+
31
+ [tool.ruff]
32
+ line-length = 120
33
+ target-version = "py310"
34
+
35
+ [tool.ruff.lint]
36
+ select = [
37
+ "E", # pycodestyle errors
38
+ "W", # pycodestyle warnings
39
+ "F", # pyflakes
40
+ "UP", # pyupgrade
41
+ "A", # flake8-builtins
42
+ "B", # flake8-bugbear
43
+ "Q", # flake8-quotes
44
+ "I", # isort
45
+ ]
46
+
47
+ [tool.pyright]
48
+ include = ["src"]
49
+ exclude = ["**/__pycache__"]
50
+
51
+ [dependency-groups]
52
+ dev = [
53
+ "pyright>=1.1.404",
54
+ "ruff>=0.12.11",
55
+ ]
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio>=5.35.0
2
+ spaces>=0.37.1
3
+ pillow>=11.3.0
4
+ gradio-image-annotation>=0.4.0
5
+ git+https://github.com/finegrain-ai/finegrain-toolbox#cf3cc389efa6eaf3e9387805981f4cefe39b3b00
6
+ git+https://github.com/finegrain-ai/finegrain-python@py310#subdirectory=finegrain
src/app.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ from functools import cache, lru_cache
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import gradio as gr
8
+ import spaces
9
+ import torch
10
+ from finegrain import CutoutResultWithImage, EditorAPIContext, ErrorResult
11
+ from finegrain_toolbox.flux import Model
12
+ from finegrain_toolbox.flux.prompt import prompt_with_embeds
13
+ from finegrain_toolbox.processors import product_placement
14
+ from gradio_image_annotation import image_annotator
15
+ from huggingface_hub import hf_hub_download
16
+ from PIL import Image
17
+ from safetensors.torch import load_file
18
+ from typing_extensions import TypeIs
19
+
20
+ # initialize on CPU then move to GPU (Zero GPU)
21
+
22
+ DEVICE_CPU = torch.device("cpu")
23
+ DTYPE = torch.bfloat16
24
+ FG_API_KEY = os.getenv("FG_API_KEY")
25
+
26
+ model = Model.from_pretrained("black-forest-labs/FLUX.1-Kontext-dev", device=DEVICE_CPU, dtype=DTYPE)
27
+
28
+ lora_path = Path(
29
+ hf_hub_download(
30
+ repo_id="finegrain/finegrain-product-placement-lora",
31
+ filename="finegrain-placement-v1-rank8.safetensors",
32
+ )
33
+ )
34
+
35
+ prompt_path = Path(
36
+ hf_hub_download(
37
+ repo_id="finegrain/finegrain-product-placement-lora",
38
+ filename="addinbox-prompt.safetensors",
39
+ )
40
+ )
41
+
42
+ prompt_st = load_file(prompt_path, device="cpu")
43
+
44
+ prompt = prompt_with_embeds(
45
+ text="Add this in the box",
46
+ clip_prompt_embeds=prompt_st["clip"],
47
+ t5_prompt_embeds=prompt_st["t5"],
48
+ )
49
+
50
+ model.transformer.load_lora_adapter(lora_path, adapter_name="placement")
51
+ model.transformer.fuse_lora()
52
+ model.transformer.unload_lora()
53
+
54
+ DEVICE = torch.device("cuda")
55
+ model = model.to(device=DEVICE, dtype=DTYPE)
56
+ prompt = prompt.to(device=DEVICE, dtype=DTYPE)
57
+
58
+
59
+ @cache
60
+ def _ctx() -> EditorAPIContext:
61
+ assert FG_API_KEY is not None
62
+ return EditorAPIContext(
63
+ api_key=FG_API_KEY,
64
+ user_agent="fg-hf-placement",
65
+ priority="low",
66
+ )
67
+
68
+
69
+ def on_change(scene: dict[str, Any] | None, reference: Image.Image | None) -> tuple[dict[str, Any], str]:
70
+ bbox_str = ""
71
+ if scene is not None and isinstance(scene.get("boxes"), list) and len(scene.get("boxes", [])) == 1:
72
+ assert scene is not None
73
+ box = scene["boxes"][0]
74
+ bbox_str = f"({box['xmin']}, {box['ymin']}, {box['xmax']}, {box['ymax']})"
75
+ return (gr.update(interactive=reference is not None and bbox_str != ""), bbox_str)
76
+
77
+
78
+ @spaces.GPU(duration=120)
79
+ def _process(
80
+ scene: dict[str, Any],
81
+ reference: Image.Image,
82
+ seed: int = 1234,
83
+ ) -> tuple[tuple[Image.Image, Image.Image], Image.Image, Image.Image]:
84
+ assert isinstance(scene_image := scene["image"], Image.Image)
85
+ assert isinstance(boxes := scene["boxes"], list)
86
+ assert len(boxes) == 1
87
+ assert isinstance(box := boxes[0], dict)
88
+ bbox = tuple(box[k] for k in ["xmin", "ymin", "xmax", "ymax"])
89
+
90
+ result = product_placement.process(
91
+ model=model,
92
+ scene=scene_image,
93
+ reference=reference,
94
+ bbox=bbox,
95
+ prompt=prompt,
96
+ seed=seed,
97
+ max_short_size=1024,
98
+ max_long_size=2048,
99
+ )
100
+
101
+ output = result.output
102
+
103
+ before_after = (scene_image.resize(output.size), output)
104
+ return (before_after, result.reference, result.scene)
105
+
106
+
107
+ def _is_error(result: Any) -> TypeIs[ErrorResult]:
108
+ if isinstance(result, ErrorResult):
109
+ raise RuntimeError(result.error)
110
+ return False
111
+
112
+
113
+ @lru_cache(maxsize=32)
114
+ def _cutout_reference(image_bytes: bytes) -> Image.Image:
115
+ async def _process(ctx: EditorAPIContext, image_bytes: bytes) -> Image.Image:
116
+ st_input = await ctx.call_async.upload_image(image_bytes)
117
+ name_r = await ctx.call_async.infer_product_name(st_input)
118
+ assert not _is_error(name_r)
119
+ bbox_r = await ctx.call_async.infer_bbox(st_input, product_name=name_r.is_product)
120
+ assert not _is_error(bbox_r)
121
+ mask_r = await ctx.call_async.segment(st_input, bbox=bbox_r.bbox)
122
+ assert not _is_error(mask_r)
123
+ cutout_r = await ctx.call_async.cutout(st_input, mask_r.state_id, with_image=True)
124
+ assert not _is_error(cutout_r)
125
+ assert isinstance(cutout_r, CutoutResultWithImage)
126
+ return Image.open(io.BytesIO(cutout_r.image))
127
+
128
+ api_ctx = _ctx()
129
+ try:
130
+ cutout = api_ctx.run_one_sync(_process, image_bytes)
131
+ except AssertionError:
132
+ api_ctx.reset()
133
+ cutout = api_ctx.run_one_sync(_process, image_bytes)
134
+
135
+ return cutout
136
+
137
+
138
+ def cutout_reference(reference: Image.Image) -> Image.Image:
139
+ buf = io.BytesIO()
140
+ reference.save(buf, format="PNG")
141
+ return _cutout_reference(buf.getvalue())
142
+
143
+
144
+ def process(
145
+ scene: dict[str, Any],
146
+ reference: Image.Image,
147
+ seed: int = 1234,
148
+ cut_out_reference: bool = False,
149
+ ) -> tuple[tuple[Image.Image, Image.Image], Image.Image, Image.Image]:
150
+ if cut_out_reference:
151
+ reference = cutout_reference(reference)
152
+
153
+ return _process(scene, reference, seed)
154
+
155
+
156
+ TITLE = """
157
+ <h1>Finegrain Product Placement LoRA</h1>
158
+
159
+ <p>
160
+ 🧪 An experiment to extend Flux Kontext with product placement capabilities.
161
+ The LoRA was trained using EditNet, our before / after image editing dataset.
162
+ </p>
163
+
164
+ <p>
165
+ Just draw a box to set where the subject should be blended, and at what size.
166
+ </p>
167
+
168
+ <p>
169
+ <a href="https://huggingface.co/finegrain/finegrain-product-placement-lora">Model Card</a> |
170
+ <a href="https://blog.finegrain.ai/posts/product-placement-flux-lora-experiment/">Blog Post</a> |
171
+ <a href="https://finegrain.ai/editnet">EditNet</a>
172
+ </p>
173
+ """
174
+
175
+ with gr.Blocks() as demo:
176
+ gr.HTML(TITLE)
177
+ with gr.Row():
178
+ with gr.Column():
179
+ scene = image_annotator(
180
+ label="Scene",
181
+ image_type="pil",
182
+ disable_edit_boxes=True,
183
+ show_download_button=False,
184
+ show_share_button=False,
185
+ single_box=True,
186
+ image_mode="RGB",
187
+ )
188
+ reference = gr.Image(
189
+ label="Product Reference",
190
+ visible=True,
191
+ interactive=True,
192
+ type="pil",
193
+ image_mode="RGBA",
194
+ )
195
+ with gr.Accordion("Options", open=False):
196
+ seed = gr.Slider(
197
+ minimum=0,
198
+ maximum=10_000,
199
+ value=1234,
200
+ step=1,
201
+ label="Seed",
202
+ )
203
+ cut_out_reference = gr.Checkbox(
204
+ label="Cut out reference",
205
+ value=bool(FG_API_KEY),
206
+ interactive=bool(FG_API_KEY),
207
+ )
208
+ with gr.Row():
209
+ run_btn = gr.ClearButton(value="Blend", interactive=False)
210
+ with gr.Column():
211
+ output_image = gr.ImageSlider(label="Output Image", show_fullscreen_button=False)
212
+ with gr.Accordion("Debug", open=False):
213
+ output_textbox = gr.Textbox(label="Bounding Box", interactive=False)
214
+ output_reference = gr.Image(
215
+ label="Reference",
216
+ visible=True,
217
+ interactive=False,
218
+ type="pil",
219
+ image_mode="RGB",
220
+ )
221
+ output_scene = gr.Image(
222
+ label="Scene",
223
+ visible=True,
224
+ interactive=False,
225
+ type="pil",
226
+ image_mode="RGB",
227
+ )
228
+
229
+ run_btn.add(output_image)
230
+
231
+ # Watch for changes (scene and reference)
232
+ # i.e. the user must select a box in the scene and upload a reference image
233
+ scene.change(fn=on_change, inputs=[scene, reference], outputs=[run_btn, output_textbox])
234
+ reference.change(fn=on_change, inputs=[scene, reference], outputs=[run_btn, output_textbox])
235
+
236
+ run_btn.click(
237
+ fn=process,
238
+ inputs=[scene, reference, seed, cut_out_reference],
239
+ outputs=[output_image, output_reference, output_scene],
240
+ )
241
+
242
+ examples = [
243
+ [
244
+ {
245
+ "image": "examples/sunglasses/scene.jpg",
246
+ "boxes": [{"xmin": 164, "ymin": 89, "xmax": 379, "ymax": 204}],
247
+ },
248
+ "examples/sunglasses/reference.webp",
249
+ ],
250
+ [
251
+ {
252
+ "image": "examples/kitchen/scene.webp",
253
+ "boxes": [{"xmin": 165, "ymin": 765, "xmax": 332, "ymax": 883}],
254
+ },
255
+ "examples/kitchen/reference.webp",
256
+ ],
257
+ [
258
+ {
259
+ "image": "examples/glass/scene.webp",
260
+ "boxes": [{"xmin": 389, "ymin": 509, "xmax": 611, "ymax": 1088}],
261
+ },
262
+ "examples/glass/reference.webp",
263
+ ],
264
+ [
265
+ {
266
+ "image": "examples/chair/scene.webp",
267
+ "boxes": [{"xmin": 366, "ymin": 389, "xmax": 623, "ymax": 728}],
268
+ },
269
+ "examples/chair/reference.webp",
270
+ ],
271
+ [
272
+ {
273
+ "image": "examples/lantern/scene.webp",
274
+ "boxes": [{"xmin": 497, "ymin": 690, "xmax": 618, "ymax": 873}],
275
+ },
276
+ "examples/lantern/reference.webp",
277
+ ],
278
+ ]
279
+
280
+ ex = gr.Examples(
281
+ examples=examples,
282
+ inputs=[scene, reference],
283
+ outputs=[output_image, output_reference, output_scene],
284
+ fn=process,
285
+ cache_examples=True,
286
+ cache_mode="eager",
287
+ )
288
+
289
+
290
+ demo.launch(show_api=False, ssr_mode=False)
uv.lock ADDED
The diff for this file is too large to render. See raw diff