tori29umai commited on
Commit
ba72a11
·
verified ·
1 Parent(s): 9d7a47a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +187 -118
app.py CHANGED
@@ -10,7 +10,6 @@ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
10
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
11
  from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
12
  import math
13
- import os
14
 
15
  # --- Model Loading ---
16
  dtype = torch.bfloat16
@@ -49,29 +48,82 @@ pipe.load_lora_weights(
49
  weight_name="qwen_lora/Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16_dim1.safetensors"
50
  )
51
  pipe.fuse_lora(lora_scale=0.7)
 
 
 
 
 
 
 
52
  pipe.transformer.__class__ = QwenImageTransformer2DModel
53
  pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
54
  optimize_pipeline_(pipe, image=[Image.new("RGB", (1024, 1024)), Image.new("RGB", (1024, 1024))], prompt="prompt")
55
 
56
  # --- Constants ---
57
  MAX_SEED = np.iinfo(np.int32).max
58
- PROMPTS = {
59
- "front": "Move the camera to a front-facing position so the full body of the character is visible. The character stands with both arms extended slightly downward and close to the thighs, keeping the body evenly balanced on both sides. The legs are positioned symmetrically with a narrow stance. The background is plain white.",
60
- "back": "Move the camera to a back-facing position so the full body of the character is visible. Background is plain white.",
61
- "left": "Move the camera to a side view (profile) from the left so the full body of the character is visible. Background is plain white.",
62
- "right": "Move the camera to a side view (profile) from the right so the full body of the character is visible. Background is plain white."
63
- }
64
 
65
- # NEW: 出力解像度リセット
66
- RESOLUTIONS = {
67
- "1:4": (512, 2048),
68
- "1:3": (576, 1728),
69
- "nealy 9:16": (768, 1344),
70
- "nealy 2:3": (832, 1216),
71
- "3:4": (896, 1152),
72
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  def _append_prompt(base: str, extra: str) -> str:
 
75
  extra = (extra or "").strip()
76
  return (base if not extra else f"{base} {extra}").strip()
77
 
@@ -88,46 +140,22 @@ def generate_single_view(input_images, prompt, seed, num_inference_steps, true_g
88
  ).images
89
  return result[0]
90
 
91
- def concat_images_horizontally(images, bg_color=(255, 255, 255)):
92
- images = [img.convert("RGB") for img in images if img is not None]
93
- if not images:
94
- return None
95
- h = max(img.height for img in images)
96
- resized = []
97
- for img in images:
98
- if img.height != h:
99
- w = int(img.width * (h / img.height))
100
- img = img.resize((w, h), Image.LANCZOS)
101
- resized.append(img)
102
- w_total = sum(img.width for img in resized)
103
- canvas = Image.new("RGB", (w_total, h), bg_color)
104
- x = 0
105
- for img in resized:
106
- canvas.paste(img, (x, 0))
107
- x += img.width
108
- return canvas
109
-
110
- # NEW: リサイズユーティリティ
111
- def resize_to_preset(img: Image.Image, preset_key: str) -> Image.Image:
112
- w, h = RESOLUTIONS[preset_key]
113
- return img.resize((w, h), Image.LANCZOS)
114
-
115
  @spaces.GPU()
116
- def generate_turnaround(
117
  image,
 
 
118
  extra_prompt="",
119
- preset_key="nealy 9:16", # NEW: デフォルト
120
  seed=42,
121
  randomize_seed=False,
122
  true_guidance_scale=1.0,
123
  num_inference_steps=4,
124
  progress=gr.Progress(track_tqdm=True),
125
  ):
126
- """4視点+横連結PNG生成(ユーザー追記プロンプト対応 & 出力解像度プリセット対応)"""
127
  if randomize_seed:
128
  seed = random.randint(0, MAX_SEED)
129
  if image is None:
130
- return None, None, None, None, None, seed, "エラー: 入力画像をアップロードしてください"
131
 
132
  if isinstance(image, Image.Image):
133
  input_image = image.convert("RGB")
@@ -136,105 +164,146 @@ def generate_turnaround(
136
 
137
  pil_images = [input_image]
138
 
139
- # 各プロンプト末尾に追記
140
- p_front = _append_prompt(PROMPTS["front"], extra_prompt)
141
- p_back = _append_prompt(PROMPTS["back"], extra_prompt)
142
- p_left = _append_prompt(PROMPTS["left"], extra_prompt)
143
- p_right = _append_prompt(PROMPTS["right"], extra_prompt)
144
-
145
- progress(0.25, desc="正面生成中...")
146
- front = generate_single_view(pil_images, p_front, seed, num_inference_steps, true_guidance_scale)
147
-
148
- progress(0.5, desc="背面生成中...")
149
- back = generate_single_view([front], p_back, seed+1, num_inference_steps, true_guidance_scale)
150
-
151
- progress(0.75, desc="左側面生成中...")
152
- left = generate_single_view([front], p_left, seed+2, num_inference_steps, true_guidance_scale)
153
-
154
- progress(1.0, desc="右側面生成中...")
155
- right = generate_single_view([front], p_right, seed+3, num_inference_steps, true_guidance_scale)
156
 
157
- # NEW: ここで指定プリセットにリサイズ
158
- front_r = resize_to_preset(front, preset_key)
159
- back_r = resize_to_preset(back, preset_key)
160
- left_r = resize_to_preset(left, preset_key)
161
- right_r = resize_to_preset(right, preset_key)
162
 
163
- # NEW: リサイズ後を連結(横:正面→右→背面→左)
164
- concat = concat_images_horizontally([front_r, right_r, back_r, left_r])
 
165
 
166
- return front_r, back_r, left_r, right_r, concat, seed, f"✅ {preset_key} にリサイズして4視点+連結画像を生成しました"
167
 
168
  # --- UI ---
169
  css = """
170
- #col-container {margin: 0 auto; max-width: 1400px;}
171
- .image-container img {object-fit: contain !important; max-width: 100%; max-height: 100%;}
172
- /* 追加: 注意ボックスのスタイル */
173
  .notice {
174
- background: #fff5f5;
175
- border: 1px solid #fca5a5;
176
- color: #7f1d1d;
177
  padding: 12px 14px;
178
- border-radius: 10px;
179
  font-weight: 600;
180
  line-height: 1.5;
181
  margin-bottom: 10px;
182
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  """
184
 
185
- with gr.Blocks(css=css) as demo:
186
- gr.Markdown("# キャクタ4視点立ち絵自動生成")
187
- gr.Markdown("アップロードしたキャラクター画像から正面・背面・左右側面、さらに4枚連結のPNG画像を出力します。")
188
-
189
- with gr.Column(elem_id="col-container"):
190
  gr.HTML(
191
  "<div class='notice'>"
192
- "注意:他者が作成した画像のアップロードはご遠慮ください。"
193
- "他人の著作物・肖像権を侵害する恐れがあります。"
194
- "当アプリ作成者は、アップロード内容による権利侵害について一切の責任を負いません。"
195
  "</div>"
196
  )
197
 
198
- input_image = gr.Image(label="入力画像", type="pil", height=500)
199
-
200
- # 追記プロンプト欄
201
- extra_prompt = gr.Textbox(
202
- label="追加プロンプト(各視点プロンプトの末尾に追記)",
203
- placeholder="例: high detail, anime style, soft lighting, 4k, pastel colors",
204
- lines=2
205
- )
206
-
207
- # NEW: 出力解像度プリセットのプルダウン
208
- preset_dropdown = gr.Dropdown(
209
- label="出力解像度プリセット",
210
- choices=list(RESOLUTIONS.keys()),
211
- value="nealy 9:16"
212
- )
213
-
214
- run_button = gr.Button("🎨 生成開始", variant="primary")
215
- status_text = gr.Textbox(label="ステータス", interactive=False)
216
-
217
- with gr.Row():
218
- result_front = gr.Image(label="正面", type="pil", format="png", height=400, show_download_button=True)
219
- result_back = gr.Image(label="背面", type="pil", format="png", height=400, show_download_button=True)
220
  with gr.Row():
221
- result_left = gr.Image(label="左側面", type="pil", format="png", height=400, show_download_button=True)
222
- result_right = gr.Image(label="右側面", type="pil", format="png", height=400, show_download_button=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
- # PNG連結出力
225
- result_concat = gr.Image(label="連結画像(正面→右→背面→左)", type="pil", format="png", height=400, show_download_button=True)
 
 
226
 
227
- with gr.Accordion("⚙️ 詳細設定", open=False):
228
- seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
229
- randomize_seed = gr.Checkbox(label="ランダムシード", value=True)
230
- true_guidance_scale = gr.Slider(label="True guidance scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
231
- num_inference_steps = gr.Slider(label="生成ステップ数", minimum=1, maximum=40, step=1, value=4)
232
 
233
- # NEW: クリック時に preset_dropdown 引数として渡す
234
  run_button.click(
235
- fn=generate_turnaround,
236
- inputs=[input_image, extra_prompt, preset_dropdown, seed, randomize_seed, true_guidance_scale, num_inference_steps],
237
- outputs=[result_front, result_back, result_left, result_right, result_concat, seed, status_text],
238
  )
239
 
240
  if __name__ == "__main__":
 
10
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
11
  from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
12
  import math
 
13
 
14
  # --- Model Loading ---
15
  dtype = torch.bfloat16
 
48
  weight_name="qwen_lora/Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16_dim1.safetensors"
49
  )
50
  pipe.fuse_lora(lora_scale=0.7)
51
+
52
+ pipe.load_lora_weights(
53
+ "dx8152/Qwen-Edit-2509-Multiple-angles",
54
+ weight_name="多角度.safetensors"
55
+ )
56
+ pipe.fuse_lora(lora_scale=1.0)
57
+
58
  pipe.transformer.__class__ = QwenImageTransformer2DModel
59
  pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
60
  optimize_pipeline_(pipe, image=[Image.new("RGB", (1024, 1024)), Image.new("RGB", (1024, 1024))], prompt="prompt")
61
 
62
  # --- Constants ---
63
  MAX_SEED = np.iinfo(np.int32).max
 
 
 
 
 
 
64
 
65
+ # プルダウンの定義(value は実際に送る中国語のみ)
66
+ CAMERA_CHOICES = [
67
+ {
68
+ "label": "镜头方向左回转45度,カメラを左に45度回転,Rotate camera 45° left,パン左45度(Pan Left 45°)",
69
+ "value": "镜头方向左回转45度",
70
+ },
71
+ {
72
+ "label": "镜头向右回转45度,カメラを右に45度回転,Rotate camera 45° right,パン右45度(Pan Right 45°)",
73
+ "value": "镜头向右回转45度",
74
+ },
75
+ {
76
+ "label": "镜头方向左回转90度,カメラを左に90度回転,Rotate camera 90° left,パン左45度(Pan Left 90°)",
77
+ "value": "镜头方向左回转90度",
78
+ },
79
+ {
80
+ "label": "镜头向右回转90度,カメラを右に90度回転,Rotate camera 90° right,パン右45度(Pan Right 90°)",
81
+ "value": "镜头向右回转90度",
82
+ },
83
+ {
84
+ "label": "将镜头转为俯视,カメラを上から見下ろす視点に切り替える,Switch to top-down view,上から全体を見下ろす(トップビュー / Top-down View)",
85
+ "value": "将镜头转为俯视",
86
+ },
87
+ {
88
+ "label": "将镜头转为仰视,カメラを下から見上げる視点に切り替える,Switch to low-angle view,被写体を下から強調して映す(ローアングル / Low Angle)",
89
+ "value": "将镜头转为仰视",
90
+ },
91
+ {
92
+ "label": "镜头转相机平面视,カメラを平面視に切り替える,Switch to orthographic view,真横からの視点(オルソビュー / Orthographic)",
93
+ "value": "镜头转相机平面视",
94
+ },
95
+ {
96
+ "label": "将镜头转为特写镜头,カメラをクローズアップに切り替える,Switch to close-up lens,被写体に寄る(望遠・ズームイン / Close-up / Zoom In)",
97
+ "value": "将镜头转为特写镜头",
98
+ },
99
+ {
100
+ "label": "将镜头转为中近景镜头,カメラをややクローズアップに切り替える,Switch to medium close-up lens,被写体の上半身や顔周辺を中心に映す(中近景 / Medium Close-up)",
101
+ "value": "将镜头转为中近景镜头",
102
+ },
103
+ {
104
+ "label": "镜头转为广角镜头,カメラをズームアウトに切り替える,Switch to wide-angle lens,広い範囲を捉える(広角 / Wide Angle)",
105
+ "value": "镜头转为广角镜头",
106
+ },
107
+ {
108
+ "label": "拉远镜头以拍摄被摄体全景,被写体の全容を映すようにカメラを引く,Pull back the camera to capture the whole subject,被写体全体を画面に収めるためにカメラを後退させる(ドリーアウト / Zoom Out / Full view)",
109
+ "value": "拉远镜头以拍摄被摄体全景",
110
+ },
111
+ {
112
+ "label": "将镜头移动到被摄体正面,カメラを被写体の正面に移動する,Move the camera to the front of the subject,被写体の正面にカメラを配置し、正面ショットを撮影する(フロントビュー / Front shot)",
113
+ "value": "将镜头移动到被摄体正面",
114
+ },
115
+ {
116
+ "label": "将镜头移动到被摄体背后,カメラを被写体の背面に移動��る,Move camera behind the subject,被写体の背後へ回り込み、後ろから撮影する(背面ショット / Behind-the-subject shot)",
117
+ "value": "将镜头移动到被摄体背后",
118
+ },
119
+ ]
120
+
121
+ # 自由入力オプション
122
+ CUSTOM_OPTION_VALUE = "__custom__"
123
+ CUSTOM_OPTION_LABEL = "自由入力 / Custom"
124
 
125
  def _append_prompt(base: str, extra: str) -> str:
126
+ """末尾にユーザー指定のプロンプトを追記(空なら変更なし)"""
127
  extra = (extra or "").strip()
128
  return (base if not extra else f"{base} {extra}").strip()
129
 
 
140
  ).images
141
  return result[0]
142
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  @spaces.GPU()
144
+ def generate_from_dropdown(
145
  image,
146
+ dropdown_value,
147
+ custom,
148
  extra_prompt="",
 
149
  seed=42,
150
  randomize_seed=False,
151
  true_guidance_scale=1.0,
152
  num_inference_steps=4,
153
  progress=gr.Progress(track_tqdm=True),
154
  ):
 
155
  if randomize_seed:
156
  seed = random.randint(0, MAX_SEED)
157
  if image is None:
158
+ return None, seed, "エラー: 入力画像をアップロードしてください", ""
159
 
160
  if isinstance(image, Image.Image):
161
  input_image = image.convert("RGB")
 
164
 
165
  pil_images = [input_image]
166
 
167
+ if dropdown_value == CUSTOM_OPTION_VALUE:
168
+ base = (custom or "").strip()
169
+ if not base:
170
+ return None, seed, "エラー: 自由入力のプロンプトを入力してください", ""
171
+ else:
172
+ base = dropdown_value or CAMERA_CHOICES[0]["value"]
 
 
 
 
 
 
 
 
 
 
 
173
 
174
+ final_prompt = _append_prompt(base, extra_prompt)
 
 
 
 
175
 
176
+ progress(0.6, desc="画像生成中...")
177
+ out = generate_single_view(pil_images, final_prompt, seed, num_inference_steps, true_guidance_scale)
178
+ progress(1.0, desc="完了")
179
 
180
+ return out, seed, "1枚生成しました(PNG)", final_prompt
181
 
182
  # --- UI ---
183
  css = """
184
+ /* レイアウトとカード風スタイル */
185
+ #app-wrap {margin: 0 auto; max-width: 1200px;}
 
186
  .notice {
187
+ background: #fff8e1;
188
+ border: 1px solid #facc15;
189
+ color: #713f12;
190
  padding: 12px 14px;
191
+ border-radius: 12px;
192
  font-weight: 600;
193
  line-height: 1.5;
194
  margin-bottom: 10px;
195
  }
196
+ .card {
197
+ background: white;
198
+ border: 1px solid #e5e7eb;
199
+ border-radius: 14px;
200
+ padding: 14px;
201
+ box-shadow: 0 1px 2px rgba(0,0,0,0.04);
202
+ }
203
+ .small {
204
+ font-size: 12px;
205
+ color: #6b7280;
206
+ }
207
+ .preview {
208
+ background: #f9fafb;
209
+ border: 1px dashed #cbd5e1;
210
+ border-radius: 10px;
211
+ padding: 8px 10px;
212
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
213
+ white-space: pre-wrap;
214
+ }
215
  """
216
 
217
+ with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
218
+ gr.Markdown("## 🎥 カメク選択")
219
+ with gr.Column(elem_id="app-wrap"):
 
 
220
  gr.HTML(
221
  "<div class='notice'>"
222
+ "注意:他者が作成した画像のアップロードはご遠慮ください。権利侵害の可能性があります。"
 
 
223
  "</div>"
224
  )
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  with gr.Row():
227
+ with gr.Column(scale=1):
228
+ input_image = gr.Image(label="入力画像", type="pil", height=420)
229
+
230
+ with gr.Column(scale=1, elem_classes=["card"]):
231
+ dropdown = gr.Dropdown(
232
+ label="カメラワーク(説明付き)",
233
+ choices=[(item["label"], item["value"]) for item in CAMERA_CHOICES] + [(CUSTOM_OPTION_LABEL, CUSTOM_OPTION_VALUE)],
234
+ value=CAMERA_CHOICES[0]["value"],
235
+ allow_custom_value=False, # ★ ユーザーは「自由入力」オプションを選んでテキストボックスに入力
236
+ interactive=True,
237
+ )
238
+
239
+ custom = gr.Textbox(
240
+ label="自由入力/中国語or英語がおすすめ",
241
+ placeholder="例: 将镜头转为斜俯视 并 拉远镜头",
242
+ visible=False,
243
+ lines=2
244
+ )
245
+
246
+ extra_prompt = gr.Textbox(
247
+ label="追加プロンプト(任意・末尾に付加/中国語or英語がおすすめ)",
248
+ placeholder="例:被摄体是一名女孩子",
249
+ lines=2
250
+ )
251
+
252
+ with gr.Row():
253
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
254
+ randomize_seed = gr.Checkbox(label="ランダムシード", value=True)
255
+
256
+ with gr.Row():
257
+ true_guidance_scale = gr.Slider(label="True guidance scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
258
+ num_inference_steps = gr.Slider(label="生成ステップ数", minimum=1, maximum=40, step=1, value=4)
259
+
260
+ run_button = gr.Button("この設定で生成", variant="primary")
261
+
262
+ # プレビュー(選択中の中国語、最終送信文)
263
+ selected = gr.Textbox(label="選択中のカメラプロンプト", value=CAMERA_CHOICES[0]["value"], interactive=False)
264
+ final_prompt_preview = gr.Textbox(label="最終的に送信されるプロンプト(+追記)", value="", interactive=False)
265
+
266
+ # 選択/入力のたびにプレビュー更新 & 自由��力欄の表示切り替え
267
+ def _sync(v, extra, custom_text):
268
+ is_custom = (v == CUSTOM_OPTION_VALUE)
269
+ base = (custom_text.strip() if is_custom else (v or CAMERA_CHOICES[0]["value"]))
270
+ # 空の自由入力はプレビューでは空表示(エラーは実行時に出す)
271
+ final = _append_prompt(base, extra) if base else ""
272
+ return (
273
+ base, # selected
274
+ final, # final_prompt_preview
275
+ gr.update(visible=is_custom), # custom visibility
276
+ )
277
+
278
+ dropdown.change(
279
+ fn=_sync,
280
+ inputs=[dropdown, extra_prompt, custom],
281
+ outputs=[selected, final_prompt_preview, custom]
282
+ )
283
+ extra_prompt.change(
284
+ fn=_sync,
285
+ inputs=[dropdown, extra_prompt, custom],
286
+ outputs=[selected, final_prompt_preview, custom]
287
+ )
288
+ custom.change(
289
+ fn=_sync,
290
+ inputs=[dropdown, extra_prompt, custom],
291
+ outputs=[selected, final_prompt_preview, custom]
292
+ )
293
 
294
+ with gr.Row():
295
+ with gr.Column(scale=1, elem_classes=["card"]):
296
+ result_image = gr.Image(label="出力画像", type="pil", format="png", height=520, show_download_button=True)
297
+ status_text = gr.Textbox(label="ステータス", interactive=False)
298
 
299
+ gr.Markdown("**送信プロンプト(確認用)**")
300
+ final_prompt_small = gr.Textbox(show_label=False, interactive=False, elem_classes=["preview", "small"])
 
 
 
301
 
302
+ # 実行ボタンの配線(custom追加で渡す
303
  run_button.click(
304
+ fn=generate_from_dropdown,
305
+ inputs=[input_image, dropdown, custom, extra_prompt, seed, randomize_seed, true_guidance_scale, num_inference_steps],
306
+ outputs=[result_image, seed, status_text, final_prompt_small],
307
  )
308
 
309
  if __name__ == "__main__":