ArtelTaleb commited on
Commit
c5dff7f
·
verified ·
1 Parent(s): abb8ecb

feat: switch to fal.ai API — fal-client + Multiple Angles LoRA

Browse files
Files changed (1) hide show
  1. app.py +44 -36
app.py CHANGED
@@ -1,8 +1,9 @@
1
  import random
2
- import torch
 
 
 
3
  import gradio as gr
4
- import spaces
5
- from diffusers import QwenImageEditPlusPipeline
6
  from PIL import Image
7
 
8
  # ── Constantes poses ──────────────────────────────────────────────────────────
@@ -21,22 +22,6 @@ ELEVATION_NAMES = {
21
  }
22
  DISTANCE_NAMES = {0.6: "close-up", 1.0: "medium shot", 1.8: "wide shot"}
23
 
24
- # ── Chargement modèle (lazy — nécessite GPU pour la quantization 4-bit) ───────
25
-
26
- dtype = torch.bfloat16
27
- pipe = None
28
-
29
-
30
- def get_pipe():
31
- global pipe
32
- if pipe is None:
33
- pipe = QwenImageEditPlusPipeline.from_pretrained(
34
- "ovedrive/Qwen-Image-Edit-2511-4bit",
35
- torch_dtype=dtype,
36
- device_map="auto",
37
- )
38
- return pipe
39
-
40
  # ── Helpers ───────────────────────────────────────────────────────────────────
41
 
42
  def snap_to_nearest(value, options):
@@ -47,15 +32,21 @@ def build_camera_prompt(azimuth: float, elevation: float, distance: float) -> st
47
  az = snap_to_nearest(azimuth, AZIMUTHS)
48
  el = snap_to_nearest(elevation, ELEVATIONS)
49
  di = snap_to_nearest(distance, DISTANCES)
50
- return f"Show this from {AZIMUTH_NAMES[az]}, {ELEVATION_NAMES[el]}, {DISTANCE_NAMES[di]}"
51
 
52
 
53
  def update_prompt_preview(azimuth, elevation, distance):
54
  return build_camera_prompt(azimuth, elevation, distance)
55
 
56
- # ── Inférence ZeroGPU ─────────────────────────────────────────────────────────
57
 
58
- @spaces.GPU(duration=120)
 
 
 
 
 
 
 
59
  def infer(
60
  image: Image.Image,
61
  azimuth: float,
@@ -64,24 +55,41 @@ def infer(
64
  seed: int,
65
  randomize_seed: bool,
66
  ):
 
 
 
67
  if randomize_seed:
68
  seed = random.randint(0, 2**31 - 1)
69
 
70
  prompt = build_camera_prompt(azimuth, elevation, distance)
71
- generator = torch.Generator(device="cpu").manual_seed(seed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- result = get_pipe()(
74
- image=[image],
75
- prompt=prompt,
76
- height=1024,
77
- width=1024,
78
- num_inference_steps=20,
79
- guidance_scale=3.5,
80
- generator=generator,
81
- num_images_per_prompt=1,
82
- ).images[0]
83
 
84
- return result, seed, prompt
85
 
86
  # ── UI Gradio ─────────────────────────────────────────────────────────────────
87
 
@@ -116,7 +124,7 @@ with gr.Blocks(title="Angle Studio") as demo:
116
 
117
  prompt_preview = gr.Textbox(
118
  label="Prompt généré / Generated prompt",
119
- value="Show this from front view, eye-level shot, medium shot",
120
  interactive=False,
121
  )
122
 
@@ -154,4 +162,4 @@ with gr.Blocks(title="Angle Studio") as demo:
154
  outputs=[output_image, output_seed, session_images, gallery],
155
  )
156
 
157
- demo.launch(theme=gr.themes.Base())
 
1
  import random
2
+ import base64
3
+ import io
4
+ import os
5
+ import fal_client
6
  import gradio as gr
 
 
7
  from PIL import Image
8
 
9
  # ── Constantes poses ──────────────────────────────────────────────────────────
 
22
  }
23
  DISTANCE_NAMES = {0.6: "close-up", 1.0: "medium shot", 1.8: "wide shot"}
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  # ── Helpers ───────────────────────────────────────────────────────────────────
26
 
27
  def snap_to_nearest(value, options):
 
32
  az = snap_to_nearest(azimuth, AZIMUTHS)
33
  el = snap_to_nearest(elevation, ELEVATIONS)
34
  di = snap_to_nearest(distance, DISTANCES)
35
+ return f"<sks> {AZIMUTH_NAMES[az]}, {ELEVATION_NAMES[el]}, {DISTANCE_NAMES[di]}"
36
 
37
 
38
  def update_prompt_preview(azimuth, elevation, distance):
39
  return build_camera_prompt(azimuth, elevation, distance)
40
 
 
41
 
42
+ def pil_to_data_uri(img: Image.Image) -> str:
43
+ buf = io.BytesIO()
44
+ img.save(buf, format="PNG")
45
+ b64 = base64.b64encode(buf.getvalue()).decode()
46
+ return f"data:image/png;base64,{b64}"
47
+
48
+ # ── Inférence fal.ai ──────────────────────────────────────────────────────────
49
+
50
  def infer(
51
  image: Image.Image,
52
  azimuth: float,
 
55
  seed: int,
56
  randomize_seed: bool,
57
  ):
58
+ if image is None:
59
+ raise gr.Error("Veuillez uploader une image source / Please upload a source image")
60
+
61
  if randomize_seed:
62
  seed = random.randint(0, 2**31 - 1)
63
 
64
  prompt = build_camera_prompt(azimuth, elevation, distance)
65
+ image_url = pil_to_data_uri(image)
66
+
67
+ os.environ["FAL_KEY"] = os.environ.get("FAL_KEY", "e5f4d316-d436-4b83-a427-bea6e535ebef:ab20bb07b0da7c4bcc1b96cec55f0d2e")
68
+
69
+ result = fal_client.run(
70
+ "fal-ai/qwen-image-edit",
71
+ arguments={
72
+ "image_url": image_url,
73
+ "prompt": prompt,
74
+ "seed": seed,
75
+ "image_size": {"width": 1024, "height": 1024},
76
+ "num_inference_steps": 4,
77
+ "guidance_scale": 1.0,
78
+ "loras": [
79
+ {
80
+ "path": "fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA",
81
+ "scale": 1.0,
82
+ }
83
+ ],
84
+ },
85
+ )
86
 
87
+ image_url_out = result["images"][0]["url"]
88
+ import urllib.request
89
+ with urllib.request.urlopen(image_url_out) as resp:
90
+ out_img = Image.open(io.BytesIO(resp.read())).convert("RGB")
 
 
 
 
 
 
91
 
92
+ return out_img, seed, prompt
93
 
94
  # ── UI Gradio ─────────────────────────────────────────────────────────────────
95
 
 
124
 
125
  prompt_preview = gr.Textbox(
126
  label="Prompt généré / Generated prompt",
127
+ value="<sks> front view, eye-level shot, medium shot",
128
  interactive=False,
129
  )
130
 
 
162
  outputs=[output_image, output_seed, session_images, gallery],
163
  )
164
 
165
+ demo.launch()