feat: update with new safeternsors

#1
Files changed (3) hide show
  1. README.md +24 -1
  2. app.py +137 -51
  3. workflow.json +11 -11
README.md CHANGED
@@ -1,14 +1,37 @@
1
  ---
2
- title: Pruna Qwen Image 2.1
3
  emoji: ⚡
4
  colorFrom: indigo
5
  colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.28.0
 
8
  app_file: app.py
9
  pinned: false
10
  hf_oauth: true
11
  hf_oauth_scopes:
12
  - inference-api
13
  - write-repos
 
 
 
 
 
 
14
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: P-Q-Image-2.1
3
  emoji: ⚡
4
  colorFrom: indigo
5
  colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.28.0
8
+ python_version: "3.12"
9
  app_file: app.py
10
  pinned: false
11
  hf_oauth: true
12
  hf_oauth_scopes:
13
  - inference-api
14
  - write-repos
15
+ short_description: Few-step LoRA for Qwen-Image-2.1 (6-steps v0.2)
16
+ models:
17
+ - PrunaAI/P-Q-Image-2.1
18
+ - Qwen/Qwen-Image-2.1
19
+ license: other
20
+ license_name: qwen-research
21
  ---
22
+
23
+ # P-Q-Image-2.1
24
+
25
+ Few-step LoRA adapters for [`Qwen/Qwen-Image-2.1`](https://huggingface.co/Qwen/Qwen-Image-2.1).
26
+ Same pipeline for text-to-image and image editing, no CFG.
27
+
28
+ Pick a **Variant**:
29
+
30
+ | Variant | Steps | Notes |
31
+ |---|:---:|---|
32
+ | `6-steps` | 6 | v0.2 default. Faithful few-step distillation; best for editing. |
33
+ | `6-steps-upsampled` | 6 | v0.2. Distills prompt upsampling into the LoRA. |
34
+ | `8-steps` | 8 | v0.1, higher quality of that release. |
35
+ | `5-steps` | 5 | v0.1, faster, lower quality. |
36
+
37
+ Model card: [`PrunaAI/P-Q-Image-2.1`](https://huggingface.co/PrunaAI/P-Q-Image-2.1)
app.py CHANGED
@@ -1,52 +1,139 @@
1
- """gr.Workflow app for PrunaAI/Pruna-Qwen-Image-2.1 (ZeroGPU).
2
 
3
  Nodes on the canvas:
4
- - "Generate Image": text-to-image with the 5- or 8-step LoRA adapter
5
  - "Edit Image": image editing with an input image + prompt
6
  """
7
 
8
  import os
9
-
10
- import torch
11
  import spaces
 
12
  import gradio as gr
13
  from gradio.utils import get_upload_folder
14
  from gradio_client import utils as client_utils
15
  from PIL import Image
16
  from diffusers import FlowMatchEulerDiscreteScheduler, QwenImage21Pipeline
17
 
18
- SIGMAS = {
19
- 5: [1.0, 0.94, 6 / 7, 2 / 3, 0.4],
20
- 8: [1.0, 14 / 15, 6 / 7, 10 / 13, 2 / 3, 6 / 11, 0.4, 2 / 9],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
 
23
- # Load the base pipeline once at module level (ZeroGPU CUDA emulation makes
24
- # this a no-op until a @spaces.GPU function actually runs).
25
  pipe = QwenImage21Pipeline.from_pretrained(
26
  "Qwen/Qwen-Image-2.1", torch_dtype=torch.bfloat16
27
- ).to("cuda")
 
 
 
 
 
 
 
28
  pipe.scheduler = FlowMatchEulerDiscreteScheduler.from_config(
29
  pipe.scheduler.config,
30
- use_dynamic_shifting=False,
31
- shift=1.0,
32
  shift_terminal=None,
33
  )
 
34
 
35
- _loaded_steps = None
36
 
37
 
38
- def _ensure_adapter(steps: int):
39
- """Load exactly one Pruna LoRA adapter matching the requested step count."""
40
- global _loaded_steps
41
- steps = 5 if steps is not None and int(steps) == 5 else 8
42
- if _loaded_steps != steps:
43
- pipe.unload_lora_weights()
44
- pipe.load_lora_weights(
45
- "PrunaAI/Pruna-Qwen-Image-2.1",
46
- weight_name=f"p_qwen_image_2.1_{steps}step_v0.1.safetensors",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  )
48
- _loaded_steps = steps
49
- return steps
50
 
51
 
52
  def _seed(seed) -> int:
@@ -78,37 +165,36 @@ def _to_file(img: Image.Image) -> dict:
78
  return {"path": path, "url": url, "is_file": True}
79
 
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  @spaces.GPU(duration=120)
82
- def generate(prompt: str, steps: int = 8, seed: int = 42) -> dict:
83
- """Text-to-image with Pruna-Qwen-Image-2.1 (5 or 8 steps, no CFG)."""
84
- steps = _ensure_adapter(steps)
85
- out = pipe(
86
- prompt=prompt,
87
- width=1024,
88
- height=1024,
89
- generator=torch.Generator("cuda").manual_seed(_seed(seed)),
90
- num_inference_steps=steps,
91
- sigmas=SIGMAS[steps],
92
- true_cfg_scale=1.0,
93
- use_kv_cache=True,
94
- ).images[0]
95
- return _to_file(out)
96
 
97
 
98
  @spaces.GPU(duration=120)
99
- def edit(image, prompt: str, steps: int = 8, seed: int = 42) -> dict:
100
- """Edit an image with Pruna-Qwen-Image-2.1 (5 or 8 steps, no CFG)."""
101
- steps = _ensure_adapter(steps)
102
- out = pipe(
103
- prompt=prompt,
104
- image=_coerce_image(image),
105
- generator=torch.Generator("cuda").manual_seed(_seed(seed)),
106
- num_inference_steps=steps,
107
- sigmas=SIGMAS[steps],
108
- true_cfg_scale=1.0,
109
- use_kv_cache=True,
110
- ).images[0]
111
- return _to_file(out)
112
 
113
 
114
  demo = gr.Workflow(
 
1
+ """gr.Workflow app for PrunaAI/P-Q-Image-2.1 (ZeroGPU).
2
 
3
  Nodes on the canvas:
4
+ - "Generate Image": text-to-image with a v0.2 or v0.1 LoRA adapter
5
  - "Edit Image": image editing with an input image + prompt
6
  """
7
 
8
  import os
 
 
9
  import spaces
10
+ import torch
11
  import gradio as gr
12
  from gradio.utils import get_upload_folder
13
  from gradio_client import utils as client_utils
14
  from PIL import Image
15
  from diffusers import FlowMatchEulerDiscreteScheduler, QwenImage21Pipeline
16
 
17
+ LORA_REPO = "PrunaAI/P-Q-Image-2.1"
18
+ DEFAULT_VARIANT = "6-steps"
19
+ LORA_STRENGTH = 1.0
20
+
21
+ # Each adapter is trained for one schedule. v0.2 uses unshifted 6-step sigmas
22
+ # plus dynamic shifting; v0.1 uses its raw nodes with shifting off.
23
+ VARIANTS = {
24
+ "6-steps": {
25
+ "weight_name": "p_q_image_2.1_6_steps_v0.2.safetensors",
26
+ "adapter_name": "step6_v02",
27
+ "steps": 6,
28
+ "sigmas": [1.0, 0.925, 0.85, 0.625, 0.375, 0.125],
29
+ "dynamic_shifting": True,
30
+ "use_kv_cache": False,
31
+ },
32
+ "6-steps-upsampled": {
33
+ "weight_name": "p_q_image_2.1_6_steps_upsampled_v0.2.safetensors",
34
+ "adapter_name": "step6_upsampled_v02",
35
+ "steps": 6,
36
+ "sigmas": [1.0, 0.925, 0.85, 0.625, 0.375, 0.125],
37
+ "dynamic_shifting": True,
38
+ "use_kv_cache": False,
39
+ },
40
+ "8-steps": {
41
+ "weight_name": "p_q_image_2.1_8_steps_v0.1.safetensors",
42
+ "adapter_name": "step8_v01",
43
+ "steps": 8,
44
+ "sigmas": [1.0, 14 / 15, 6 / 7, 10 / 13, 2 / 3, 6 / 11, 0.4, 2 / 9],
45
+ "dynamic_shifting": False,
46
+ "use_kv_cache": True,
47
+ },
48
+ "5-steps": {
49
+ "weight_name": "p_q_image_2.1_5_steps_v0.1.safetensors",
50
+ "adapter_name": "step5_v01",
51
+ "steps": 5,
52
+ "sigmas": [1.0, 0.94, 6 / 7, 2 / 3, 0.4],
53
+ "dynamic_shifting": False,
54
+ "use_kv_cache": True,
55
+ },
56
+ }
57
+
58
+ VARIANT_ALIASES = {
59
+ "6-steps": "6-steps",
60
+ "6-step": "6-steps",
61
+ "6step": "6-steps",
62
+ "6": "6-steps",
63
+ "basic": "6-steps",
64
+ "v0.2": "6-steps",
65
+ "v0.2-6-step": "6-steps",
66
+ "v0.2-6-steps": "6-steps",
67
+ "6-steps-upsampled": "6-steps-upsampled",
68
+ "6-step-upsampled": "6-steps-upsampled",
69
+ "6step-upsampled": "6-steps-upsampled",
70
+ "prompt-upsampled": "6-steps-upsampled",
71
+ "upsampled": "6-steps-upsampled",
72
+ "upsample": "6-steps-upsampled",
73
+ "v0.2-upsampled": "6-steps-upsampled",
74
+ "v0.2-6-step-upsampled": "6-steps-upsampled",
75
+ "v0.2-6-steps-upsampled": "6-steps-upsampled",
76
+ "8-steps": "8-steps",
77
+ "8-step": "8-steps",
78
+ "8step": "8-steps",
79
+ "8": "8-steps",
80
+ "v0.1-8-step": "8-steps",
81
+ "v0.1-8-steps": "8-steps",
82
+ "5-steps": "5-steps",
83
+ "5-step": "5-steps",
84
+ "5step": "5-steps",
85
+ "5": "5-steps",
86
+ "v0.1-5-step": "5-steps",
87
+ "v0.1-5-steps": "5-steps",
88
  }
89
 
 
 
90
  pipe = QwenImage21Pipeline.from_pretrained(
91
  "Qwen/Qwen-Image-2.1", torch_dtype=torch.bfloat16
92
+ )
93
+ for spec in VARIANTS.values():
94
+ pipe.load_lora_weights(
95
+ LORA_REPO,
96
+ weight_name=spec["weight_name"],
97
+ adapter_name=spec["adapter_name"],
98
+ )
99
+ pipe.set_adapters([VARIANTS[DEFAULT_VARIANT]["adapter_name"]], [LORA_STRENGTH])
100
  pipe.scheduler = FlowMatchEulerDiscreteScheduler.from_config(
101
  pipe.scheduler.config,
102
+ use_dynamic_shifting=True,
 
103
  shift_terminal=None,
104
  )
105
+ pipe = pipe.to("cuda")
106
 
107
+ _active_variant = DEFAULT_VARIANT
108
 
109
 
110
+ def _resolve_variant(variant) -> str:
111
+ if variant is None or variant == "":
112
+ return DEFAULT_VARIANT
113
+ key = "-".join(str(variant).strip().lower().replace("_", "-").split())
114
+ try:
115
+ key = str(int(float(key)))
116
+ except (TypeError, ValueError):
117
+ pass
118
+ return VARIANT_ALIASES.get(key, DEFAULT_VARIANT)
119
+
120
+
121
+ def _ensure_variant(variant) -> dict:
122
+ """Activate the matching adapter and scheduler. Load only one at a time."""
123
+ global _active_variant
124
+ name = _resolve_variant(variant)
125
+ spec = VARIANTS[name]
126
+ if _active_variant != name:
127
+ pipe.set_adapters([spec["adapter_name"]], [LORA_STRENGTH])
128
+ scheduler_kwargs = {"use_dynamic_shifting": spec["dynamic_shifting"], "shift_terminal": None}
129
+ if not spec["dynamic_shifting"]:
130
+ scheduler_kwargs["shift"] = 1.0
131
+ pipe.scheduler = FlowMatchEulerDiscreteScheduler.from_config(
132
+ pipe.scheduler.config,
133
+ **scheduler_kwargs,
134
  )
135
+ _active_variant = name
136
+ return spec
137
 
138
 
139
  def _seed(seed) -> int:
 
165
  return {"path": path, "url": url, "is_file": True}
166
 
167
 
168
+ def _run_pipe(prompt: str, spec: dict, seed: int, image=None):
169
+ kwargs = {
170
+ "prompt": prompt,
171
+ "generator": torch.Generator("cuda").manual_seed(_seed(seed)),
172
+ "num_inference_steps": spec["steps"],
173
+ "sigmas": spec["sigmas"],
174
+ "true_cfg_scale": 1.0,
175
+ "use_kv_cache": spec["use_kv_cache"],
176
+ "output_resolution": 1024,
177
+ }
178
+ if image is None:
179
+ kwargs["width"] = 1024
180
+ kwargs["height"] = 1024
181
+ else:
182
+ kwargs["image"] = image
183
+ return pipe(**kwargs).images[0]
184
+
185
+
186
  @spaces.GPU(duration=120)
187
+ def generate(prompt: str, variant: str = DEFAULT_VARIANT, seed: int = 42) -> dict:
188
+ """Text-to-image with P-Q-Image-2.1 (no CFG)."""
189
+ spec = _ensure_variant(variant)
190
+ return _to_file(_run_pipe(prompt, spec, seed))
 
 
 
 
 
 
 
 
 
 
191
 
192
 
193
  @spaces.GPU(duration=120)
194
+ def edit(image, prompt: str, variant: str = DEFAULT_VARIANT, seed: int = 42) -> dict:
195
+ """Edit an image with P-Q-Image-2.1 (no CFG)."""
196
+ spec = _ensure_variant(variant)
197
+ return _to_file(_run_pipe(prompt, spec, seed, image=_coerce_image(image)))
 
 
 
 
 
 
 
 
 
198
 
199
 
200
  demo = gr.Workflow(
workflow.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "schema_version": "2",
3
- "name": "Pruna Qwen Image 2.1",
4
  "references": [
5
  {
6
  "id": "ref_prompt",
@@ -14,15 +14,15 @@
14
  "data": {"out": "A glowing neon shop sign that reads \"QWEN IMAGE 2.1\", mounted on a brick wall in a narrow city alley at night. Heavy rain, wet pavement reflecting pink and blue light, shallow depth of field, cinematic photograph."}
15
  },
16
  {
17
- "id": "ref_steps",
18
- "label": "Steps (5 or 8)",
19
  "role": "reference",
20
- "asset_type": "number",
21
  "x": 40,
22
  "y": 320,
23
- "inputs": [{"id": "in", "label": "Number", "type": "number"}],
24
- "outputs": [{"id": "out", "label": "Number", "type": "number"}],
25
- "data": {"out": 8}
26
  },
27
  {
28
  "id": "ref_image",
@@ -68,7 +68,7 @@
68
  "y": 100,
69
  "inputs": [
70
  {"id": "prompt", "label": "Prompt", "type": "text", "required": true},
71
- {"id": "steps", "label": "Steps", "type": "number"},
72
  {"id": "seed", "label": "Seed", "type": "number"}
73
  ],
74
  "outputs": [{"id": "out_0", "label": "Image", "type": "image", "output_index": 0}]
@@ -84,7 +84,7 @@
84
  "inputs": [
85
  {"id": "image", "label": "Image", "type": "image", "required": true},
86
  {"id": "prompt", "label": "Edit Prompt", "type": "text", "required": true},
87
- {"id": "steps", "label": "Steps", "type": "number"},
88
  {"id": "seed", "label": "Seed", "type": "number"}
89
  ],
90
  "outputs": [{"id": "out_0", "label": "Edited Image", "type": "image", "output_index": 0}]
@@ -114,12 +114,12 @@
114
  ],
115
  "edges": [
116
  {"id": "e1", "from_node_id": "ref_prompt", "from_port_id": "out", "to_node_id": "op_generate", "to_port_id": "prompt", "type": "text"},
117
- {"id": "e2", "from_node_id": "ref_steps", "from_port_id": "out", "to_node_id": "op_generate", "to_port_id": "steps", "type": "number"},
118
  {"id": "e3", "from_node_id": "ref_seed", "from_port_id": "out", "to_node_id": "op_generate", "to_port_id": "seed", "type": "number"},
119
  {"id": "e4", "from_node_id": "ref_image", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "image", "type": "image"},
120
  {"id": "e7", "from_node_id": "ref_edit_prompt", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "prompt", "type": "text"},
121
  {"id": "e5", "from_node_id": "op_generate", "from_port_id": "out_0", "to_node_id": "sub_generated", "to_port_id": "in", "type": "image"},
122
- {"id": "e8", "from_node_id": "ref_steps", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "steps", "type": "number"},
123
  {"id": "e9", "from_node_id": "ref_seed", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "seed", "type": "number"},
124
  {"id": "e6", "from_node_id": "op_edit", "from_port_id": "out_0", "to_node_id": "sub_edited", "to_port_id": "in", "type": "image"}
125
  ]
 
1
  {
2
  "schema_version": "2",
3
+ "name": "P-Q-Image-2.1",
4
  "references": [
5
  {
6
  "id": "ref_prompt",
 
14
  "data": {"out": "A glowing neon shop sign that reads \"QWEN IMAGE 2.1\", mounted on a brick wall in a narrow city alley at night. Heavy rain, wet pavement reflecting pink and blue light, shallow depth of field, cinematic photograph."}
15
  },
16
  {
17
+ "id": "ref_variant",
18
+ "label": "Variant (6-steps | 6-steps-upsampled | 8-steps | 5-steps)",
19
  "role": "reference",
20
+ "asset_type": "text",
21
  "x": 40,
22
  "y": 320,
23
+ "inputs": [{"id": "in", "label": "Text", "type": "text"}],
24
+ "outputs": [{"id": "out", "label": "Text", "type": "text"}],
25
+ "data": {"out": "6-steps"}
26
  },
27
  {
28
  "id": "ref_image",
 
68
  "y": 100,
69
  "inputs": [
70
  {"id": "prompt", "label": "Prompt", "type": "text", "required": true},
71
+ {"id": "variant", "label": "Variant", "type": "text"},
72
  {"id": "seed", "label": "Seed", "type": "number"}
73
  ],
74
  "outputs": [{"id": "out_0", "label": "Image", "type": "image", "output_index": 0}]
 
84
  "inputs": [
85
  {"id": "image", "label": "Image", "type": "image", "required": true},
86
  {"id": "prompt", "label": "Edit Prompt", "type": "text", "required": true},
87
+ {"id": "variant", "label": "Variant", "type": "text"},
88
  {"id": "seed", "label": "Seed", "type": "number"}
89
  ],
90
  "outputs": [{"id": "out_0", "label": "Edited Image", "type": "image", "output_index": 0}]
 
114
  ],
115
  "edges": [
116
  {"id": "e1", "from_node_id": "ref_prompt", "from_port_id": "out", "to_node_id": "op_generate", "to_port_id": "prompt", "type": "text"},
117
+ {"id": "e2", "from_node_id": "ref_variant", "from_port_id": "out", "to_node_id": "op_generate", "to_port_id": "variant", "type": "text"},
118
  {"id": "e3", "from_node_id": "ref_seed", "from_port_id": "out", "to_node_id": "op_generate", "to_port_id": "seed", "type": "number"},
119
  {"id": "e4", "from_node_id": "ref_image", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "image", "type": "image"},
120
  {"id": "e7", "from_node_id": "ref_edit_prompt", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "prompt", "type": "text"},
121
  {"id": "e5", "from_node_id": "op_generate", "from_port_id": "out_0", "to_node_id": "sub_generated", "to_port_id": "in", "type": "image"},
122
+ {"id": "e8", "from_node_id": "ref_variant", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "variant", "type": "text"},
123
  {"id": "e9", "from_node_id": "ref_seed", "from_port_id": "out", "to_node_id": "op_edit", "to_port_id": "seed", "type": "number"},
124
  {"id": "e6", "from_node_id": "op_edit", "from_port_id": "out_0", "to_node_id": "sub_edited", "to_port_id": "in", "type": "image"}
125
  ]