Update app.py
Browse files
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 |
-
#
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 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
|
| 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,
|
| 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 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 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 |
-
|
| 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 |
-
|
| 164 |
-
|
|
|
|
| 165 |
|
| 166 |
-
return
|
| 167 |
|
| 168 |
# --- UI ---
|
| 169 |
css = """
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
/* 追加: 注意ボックスのスタイル */
|
| 173 |
.notice {
|
| 174 |
-
background: #
|
| 175 |
-
border: 1px solid #
|
| 176 |
-
color: #
|
| 177 |
padding: 12px 14px;
|
| 178 |
-
border-radius:
|
| 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("#
|
| 187 |
-
gr.
|
| 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 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
| 226 |
|
| 227 |
-
|
| 228 |
-
|
| 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 |
-
#
|
| 234 |
run_button.click(
|
| 235 |
-
fn=
|
| 236 |
-
inputs=[input_image,
|
| 237 |
-
outputs=[
|
| 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__":
|