Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -15,7 +15,6 @@ import math
|
|
| 15 |
dtype = torch.bfloat16
|
| 16 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 17 |
|
| 18 |
-
# Scheduler configuration for Lightning
|
| 19 |
scheduler_config = {
|
| 20 |
"base_image_seq_len": 256,
|
| 21 |
"base_shift": math.log(3),
|
|
@@ -32,11 +31,8 @@ scheduler_config = {
|
|
| 32 |
"use_exponential_sigmas": False,
|
| 33 |
"use_karras_sigmas": False,
|
| 34 |
}
|
| 35 |
-
|
| 36 |
-
# Initialize scheduler
|
| 37 |
scheduler = FlowMatchEulerDiscreteScheduler.from_config(scheduler_config)
|
| 38 |
|
| 39 |
-
# Load model
|
| 40 |
pipe = QwenImageEditPlusPipeline.from_pretrained(
|
| 41 |
"Qwen/Qwen-Image-Edit-2509",
|
| 42 |
scheduler=scheduler,
|
|
@@ -62,68 +58,127 @@ optimize_pipeline_(pipe, image=[Image.new("RGB", (1024, 1024)), Image.new("RGB",
|
|
| 62 |
# --- Constants ---
|
| 63 |
MAX_SEED = np.iinfo(np.int32).max
|
| 64 |
|
| 65 |
-
#
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
},
|
| 75 |
-
{
|
| 76 |
-
"
|
| 77 |
-
"
|
|
|
|
| 78 |
},
|
| 79 |
-
{
|
| 80 |
-
|
| 81 |
-
"
|
|
|
|
|
|
|
| 82 |
},
|
| 83 |
-
{
|
| 84 |
-
"
|
| 85 |
-
"
|
|
|
|
| 86 |
},
|
| 87 |
-
{
|
| 88 |
-
"
|
| 89 |
-
"
|
|
|
|
| 90 |
},
|
| 91 |
-
{
|
| 92 |
-
"
|
| 93 |
-
"
|
|
|
|
| 94 |
},
|
| 95 |
-
{
|
| 96 |
-
"
|
| 97 |
-
"
|
|
|
|
| 98 |
},
|
| 99 |
-
{
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
},
|
| 103 |
-
{
|
| 104 |
-
"
|
| 105 |
-
"
|
|
|
|
| 106 |
},
|
| 107 |
-
{
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
},
|
| 111 |
-
{
|
| 112 |
-
"
|
| 113 |
-
"
|
|
|
|
| 114 |
},
|
| 115 |
-
{
|
| 116 |
-
"
|
| 117 |
-
"
|
|
|
|
| 118 |
},
|
| 119 |
-
|
|
|
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|
|
@@ -143,19 +198,21 @@ def generate_single_view(input_images, prompt, seed, num_inference_steps, true_g
|
|
| 143 |
@spaces.GPU()
|
| 144 |
def generate_from_dropdown(
|
| 145 |
image,
|
| 146 |
-
|
| 147 |
-
|
| 148 |
extra_prompt="",
|
| 149 |
-
seed=
|
| 150 |
-
randomize_seed=
|
| 151 |
-
true_guidance_scale=
|
| 152 |
-
num_inference_steps=
|
|
|
|
| 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,
|
| 159 |
|
| 160 |
if isinstance(image, Image.Image):
|
| 161 |
input_image = image.convert("RGB")
|
|
@@ -164,24 +221,23 @@ def generate_from_dropdown(
|
|
| 164 |
|
| 165 |
pil_images = [input_image]
|
| 166 |
|
| 167 |
-
if
|
| 168 |
-
|
| 169 |
-
if not
|
| 170 |
-
return None,
|
| 171 |
else:
|
| 172 |
-
|
| 173 |
|
| 174 |
-
final_prompt = _append_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,
|
| 181 |
|
| 182 |
# --- UI ---
|
| 183 |
css = """
|
| 184 |
-
/* レイアウトとカード風スタイル */
|
| 185 |
#app-wrap {margin: 0 auto; max-width: 1200px;}
|
| 186 |
.notice {
|
| 187 |
background: #fff8e1;
|
|
@@ -200,10 +256,7 @@ css = """
|
|
| 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;
|
|
@@ -215,95 +268,141 @@ css = """
|
|
| 215 |
"""
|
| 216 |
|
| 217 |
with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 229 |
|
| 230 |
with gr.Column(scale=1, elem_classes=["card"]):
|
| 231 |
dropdown = gr.Dropdown(
|
| 232 |
-
label="
|
| 233 |
-
choices=
|
| 234 |
-
value=
|
| 235 |
-
allow_custom_value=False,
|
| 236 |
interactive=True,
|
| 237 |
)
|
| 238 |
|
| 239 |
-
|
| 240 |
-
label="
|
| 241 |
-
placeholder="
|
| 242 |
visible=False,
|
| 243 |
lines=2
|
| 244 |
)
|
| 245 |
|
| 246 |
extra_prompt = gr.Textbox(
|
| 247 |
-
label="
|
| 248 |
-
placeholder="
|
| 249 |
lines=2
|
| 250 |
)
|
| 251 |
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
num_inference_steps = gr.Slider(label="生成ステップ数", minimum=1, maximum=40, step=1, value=4)
|
| 259 |
|
| 260 |
-
run_button = gr.Button("
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
final_prompt_preview = gr.Textbox(label="最終的に送信されるプロンプト(+追記)", value="", interactive=False)
|
| 265 |
|
| 266 |
-
# 選択/入力のたびにプレビュー更新 &
|
| 267 |
-
def _sync(
|
| 268 |
-
is_custom = (
|
| 269 |
-
base = (custom_text.strip() if is_custom else (
|
| 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,
|
| 281 |
-
outputs=[
|
| 282 |
)
|
| 283 |
extra_prompt.change(
|
| 284 |
fn=_sync,
|
| 285 |
-
inputs=[dropdown, extra_prompt,
|
| 286 |
-
outputs=[
|
| 287 |
)
|
| 288 |
-
|
| 289 |
fn=_sync,
|
| 290 |
-
inputs=[dropdown, extra_prompt,
|
| 291 |
-
outputs=[
|
| 292 |
)
|
| 293 |
|
| 294 |
with gr.Row():
|
| 295 |
with gr.Column(scale=1, elem_classes=["card"]):
|
| 296 |
-
result_image = gr.Image(label="
|
| 297 |
-
status_text = gr.Textbox(label="
|
| 298 |
-
|
| 299 |
-
gr.Markdown("**送信プロンプト(確認用)**")
|
| 300 |
final_prompt_small = gr.Textbox(show_label=False, interactive=False, elem_classes=["preview", "small"])
|
| 301 |
|
| 302 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
run_button.click(
|
| 304 |
fn=generate_from_dropdown,
|
| 305 |
-
inputs=[input_image, dropdown,
|
| 306 |
-
outputs=[result_image,
|
| 307 |
)
|
| 308 |
|
| 309 |
if __name__ == "__main__":
|
|
|
|
| 15 |
dtype = torch.bfloat16
|
| 16 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 17 |
|
|
|
|
| 18 |
scheduler_config = {
|
| 19 |
"base_image_seq_len": 256,
|
| 20 |
"base_shift": math.log(3),
|
|
|
|
| 31 |
"use_exponential_sigmas": False,
|
| 32 |
"use_karras_sigmas": False,
|
| 33 |
}
|
|
|
|
|
|
|
| 34 |
scheduler = FlowMatchEulerDiscreteScheduler.from_config(scheduler_config)
|
| 35 |
|
|
|
|
| 36 |
pipe = QwenImageEditPlusPipeline.from_pretrained(
|
| 37 |
"Qwen/Qwen-Image-Edit-2509",
|
| 38 |
scheduler=scheduler,
|
|
|
|
| 58 |
# --- Constants ---
|
| 59 |
MAX_SEED = np.iinfo(np.int32).max
|
| 60 |
|
| 61 |
+
# 内部デフォルト(アコーディオンの初期値にも使用)
|
| 62 |
+
DEFAULT_SEED = 0
|
| 63 |
+
DEFAULT_RANDOMIZE = True
|
| 64 |
+
DEFAULT_TRUE_GUIDANCE_SCALE = 1.0
|
| 65 |
+
DEFAULT_NUM_INFERENCE_STEPS = 4
|
| 66 |
+
|
| 67 |
+
# カメラオプション(送信値は常に 'cn')
|
| 68 |
+
CAMERA_OPTIONS = [
|
| 69 |
+
{"cn": "镜头方向左回转45度", "ja": "カメラを左に45度回転", "en": "Rotate camera 45° left"},
|
| 70 |
+
{"cn": "镜头向右回转45度", "ja": "カメラを右に45度回転", "en": "Rotate camera 45° right"},
|
| 71 |
+
{"cn": "镜头方向左回转90度", "ja": "カメラを左に90度回転", "en": "Rotate camera 90° left"},
|
| 72 |
+
{"cn": "镜头向右回转90度", "ja": "カメラを右に90度回転", "en": "Rotate camera 90° right"},
|
| 73 |
+
{"cn": "将镜头转为俯视", "ja": "カメラを上から見下ろす視点に切り替える", "en": "Switch to top-down view"},
|
| 74 |
+
{"cn": "将镜头转为仰视", "ja": "カメラを下から見上げる視点に切り替える", "en": "Switch to low-angle view"},
|
| 75 |
+
{"cn": "镜头转相机平面视", "ja": "カメラを平面視に切り替える", "en": "Switch to orthographic view"},
|
| 76 |
+
{"cn": "将镜头转为特写镜头", "ja": "カメラをクローズアップに切り替える", "en": "Switch to close-up lens"},
|
| 77 |
+
{"cn": "将镜头转为中近景镜头", "ja": "カメラをややクローズアップに切り替える", "en": "Switch to medium close-up lens"},
|
| 78 |
+
{"cn": "镜头转为广角镜头", "ja": "カメラをズームアウトに切り替える", "en": "Switch to wide-angle lens"},
|
| 79 |
+
{"cn": "拉远镜头以拍摄被摄体全景", "ja": "被写体の全容を映すようにカメラを引く", "en": "Pull back the camera to capture the whole subject"},
|
| 80 |
+
{"cn": "将镜头移动到被摄体正面", "ja": "カメラを被写体の正面に移動する", "en": "Move the camera to the front of the subject"},
|
| 81 |
+
{"cn": "将镜头移动到被摄体背后", "ja": "カメラを被写体の背面に移動する", "en": "Move camera behind the subject"},
|
| 82 |
+
]
|
| 83 |
+
|
| 84 |
+
# 自由入力オプション(言語別表示)
|
| 85 |
+
CUSTOM_OPTION_VALUE = "__custom__"
|
| 86 |
+
CUSTOM_LABELS = {
|
| 87 |
+
"en": "Custom (enter Chinese prompt)",
|
| 88 |
+
"ja": "自由入力(中国語で入力)",
|
| 89 |
+
"zh": "自定义(用中文输入)",
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
# i18n 辞書
|
| 93 |
+
I18N = {
|
| 94 |
+
"title": {
|
| 95 |
+
"en": "Camera Work ",
|
| 96 |
+
"ja": "カメラワーク",
|
| 97 |
+
"zh": "镜头控制",
|
| 98 |
},
|
| 99 |
+
"notice": {
|
| 100 |
+
"en": "Note: Please avoid uploading images created by others. There may be rights infringements.",
|
| 101 |
+
"ja": "注意:他者が作成した画像のアップロードはご遠慮ください。権利侵害の可能性があります。",
|
| 102 |
+
"zh": "注意:请勿上传他人创作的图片,可能涉及权利侵害。",
|
| 103 |
},
|
| 104 |
+
"input_image": {"en": "Input image", "ja": "入力画像", "zh": "输入图像"},
|
| 105 |
+
"dropdown_label": {
|
| 106 |
+
"en": "Camera work (label shows CN + selected language)",
|
| 107 |
+
"ja": "カメラワーク(表示は 中国語+選択言語)",
|
| 108 |
+
"zh": "镜头操作(显示为 中文+所选语言)",
|
| 109 |
},
|
| 110 |
+
"custom_cn_label": {
|
| 111 |
+
"en": "Custom Chinese prompt",
|
| 112 |
+
"ja": "自由入力の中国語プロンプト",
|
| 113 |
+
"zh": "自定义中文提示词",
|
| 114 |
},
|
| 115 |
+
"custom_cn_ph": {
|
| 116 |
+
"en": "e.g., 将镜头转为斜俯视 并 拉远镜头",
|
| 117 |
+
"ja": "例: 将镜头转为斜俯视 并 拉远镜头",
|
| 118 |
+
"zh": "例如:将镜头转为斜俯视 并 拉远镜头",
|
| 119 |
},
|
| 120 |
+
"extra_label": {
|
| 121 |
+
"en": "Extra prompt (optional, appended at end)",
|
| 122 |
+
"ja": "追加プロンプト(任意・末尾に付加)",
|
| 123 |
+
"zh": "附加提示词(可选,追加在末尾)",
|
| 124 |
},
|
| 125 |
+
"extra_ph": {
|
| 126 |
+
"en": "e.g., high detail, soft lighting, anime style, 4k",
|
| 127 |
+
"ja": "例: high detail, soft lighting, anime style, 4k",
|
| 128 |
+
"zh": "例如:high detail, soft lighting, anime style, 4k",
|
| 129 |
},
|
| 130 |
+
"accordion": {"en": "Show advanced settings", "ja": "詳細設定を開く", "zh": "展开高级设置"},
|
| 131 |
+
"seed": {"en": "Seed", "ja": "Seed", "zh": "Seed"},
|
| 132 |
+
"rand": {"en": "Randomize seed", "ja": "ランダムシード", "zh": "随机种子"},
|
| 133 |
+
"tgs": {"en": "True guidance scale", "ja": "True guidance scale", "zh": "True guidance scale"},
|
| 134 |
+
"steps": {"en": "Steps", "ja": "生成ステップ数", "zh": "生成步数"},
|
| 135 |
+
"run": {"en": "Generate", "ja": "生成", "zh": "生成"},
|
| 136 |
+
"sel_cn": {
|
| 137 |
+
"en": "Selected camera prompt (to be sent, Chinese)",
|
| 138 |
+
"ja": "選択中のカメラプロンプト(送信対象・中国語)",
|
| 139 |
+
"zh": "所选镜头提示(发送内容,中文)",
|
| 140 |
},
|
| 141 |
+
"final_prev": {
|
| 142 |
+
"en": "Final prompt to be sent (Chinese + extra)",
|
| 143 |
+
"ja": "最終的に送信されるプロンプト(中国語+追記)",
|
| 144 |
+
"zh": "最终发送的提示(中文+附加)",
|
| 145 |
},
|
| 146 |
+
"output": {"en": "Output image", "ja": "出力画像", "zh": "输出图像"},
|
| 147 |
+
"status": {"en": "Status", "ja": "ステータス", "zh": "状态"},
|
| 148 |
+
"sent_hdr": {"en": "**Prompt sent (for reference)**", "ja": "**送信プロンプト(確認用)**", "zh": "**已发送提示(供参考)**"},
|
| 149 |
+
"status_ok": {
|
| 150 |
+
"en": "✅ Generated 1 image with Chinese prompt (PNG).",
|
| 151 |
+
"ja": "✅ 中国語プロンプトで1枚生成しました(PNG)。",
|
| 152 |
+
"zh": "✅ 使用中文提示生成了 1 张图片(PNG)。",
|
| 153 |
},
|
| 154 |
+
"err_no_img": {
|
| 155 |
+
"en": "Error: Please upload an input image.",
|
| 156 |
+
"ja": "エラー: 入力画像をアップロードしてください",
|
| 157 |
+
"zh": "错误:请先上传输入图像。",
|
| 158 |
},
|
| 159 |
+
"err_no_custom": {
|
| 160 |
+
"en": "Error: Please enter a custom Chinese prompt.",
|
| 161 |
+
"ja": "エラー: 自由入力の中国語プロンプトを入力してください",
|
| 162 |
+
"zh": "错误:请输入自定义中文提示词。",
|
| 163 |
},
|
| 164 |
+
"lang_label": {"en": "UI Language", "ja": "UI言語", "zh": "界面语言"},
|
| 165 |
+
}
|
| 166 |
|
| 167 |
+
def t(key, lang):
|
| 168 |
+
return I18N[key][lang]
|
| 169 |
+
|
| 170 |
+
def build_dropdown_choices(lang):
|
| 171 |
+
# ラベルは「中国語 + (選択言語の説明)」で表示、送信値は常に中国語
|
| 172 |
+
if lang not in ("en", "ja", "zh"):
|
| 173 |
+
lang = "en"
|
| 174 |
+
ch = []
|
| 175 |
+
for item in CAMERA_OPTIONS:
|
| 176 |
+
label = f"{item['cn']}, {item[lang]}"
|
| 177 |
+
ch.append((label, item["cn"]))
|
| 178 |
+
ch.append((CUSTOM_LABELS[lang], CUSTOM_OPTION_VALUE))
|
| 179 |
+
return ch
|
| 180 |
|
| 181 |
def _append_prompt(base: str, extra: str) -> str:
|
|
|
|
| 182 |
extra = (extra or "").strip()
|
| 183 |
return (base if not extra else f"{base} {extra}").strip()
|
| 184 |
|
|
|
|
| 198 |
@spaces.GPU()
|
| 199 |
def generate_from_dropdown(
|
| 200 |
image,
|
| 201 |
+
dropdown_value_cn,
|
| 202 |
+
custom_cn,
|
| 203 |
extra_prompt="",
|
| 204 |
+
seed=DEFAULT_SEED,
|
| 205 |
+
randomize_seed=DEFAULT_RANDOMIZE,
|
| 206 |
+
true_guidance_scale=DEFAULT_TRUE_GUIDANCE_SCALE,
|
| 207 |
+
num_inference_steps=DEFAULT_NUM_INFERENCE_STEPS,
|
| 208 |
+
lang="en",
|
| 209 |
progress=gr.Progress(track_tqdm=True),
|
| 210 |
):
|
| 211 |
if randomize_seed:
|
| 212 |
seed = random.randint(0, MAX_SEED)
|
| 213 |
+
|
| 214 |
if image is None:
|
| 215 |
+
return None, t("err_no_img", lang), ""
|
| 216 |
|
| 217 |
if isinstance(image, Image.Image):
|
| 218 |
input_image = image.convert("RGB")
|
|
|
|
| 221 |
|
| 222 |
pil_images = [input_image]
|
| 223 |
|
| 224 |
+
if dropdown_value_cn == CUSTOM_OPTION_VALUE:
|
| 225 |
+
base_cn = (custom_cn or "").strip()
|
| 226 |
+
if not base_cn:
|
| 227 |
+
return None, t("err_no_custom", lang), ""
|
| 228 |
else:
|
| 229 |
+
base_cn = dropdown_value_cn or CAMERA_OPTIONS[0]["cn"]
|
| 230 |
|
| 231 |
+
final_prompt = _append_prompt(base_cn, extra_prompt)
|
| 232 |
|
| 233 |
+
progress(0.6, desc="Generating..." if lang=="en" else ("生成中..." if lang=="ja" else "生成中..."))
|
| 234 |
out = generate_single_view(pil_images, final_prompt, seed, num_inference_steps, true_guidance_scale)
|
| 235 |
+
progress(1.0, desc="Done" if lang=="en" else ("完了" if lang=="ja" else "完成"))
|
| 236 |
|
| 237 |
+
return out, t("status_ok", lang), final_prompt
|
| 238 |
|
| 239 |
# --- UI ---
|
| 240 |
css = """
|
|
|
|
| 241 |
#app-wrap {margin: 0 auto; max-width: 1200px;}
|
| 242 |
.notice {
|
| 243 |
background: #fff8e1;
|
|
|
|
| 256 |
padding: 14px;
|
| 257 |
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
| 258 |
}
|
| 259 |
+
.small { font-size: 12px; color: #6b7280; }
|
|
|
|
|
|
|
|
|
|
| 260 |
.preview {
|
| 261 |
background: #f9fafb;
|
| 262 |
border: 1px dashed #cbd5e1;
|
|
|
|
| 268 |
"""
|
| 269 |
|
| 270 |
with gr.Blocks(css=css, theme=gr.themes.Soft()) as demo:
|
| 271 |
+
# 言語選択(デフォルト英語)
|
| 272 |
+
lang_selector = gr.Radio(
|
| 273 |
+
label=I18N["lang_label"]["en"],
|
| 274 |
+
choices=[("English", "en"), ("日本語", "ja"), ("中文", "zh")],
|
| 275 |
+
value="en",
|
| 276 |
+
interactive=True,
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
title_md = gr.Markdown(I18N["title"]["en"])
|
| 280 |
+
|
| 281 |
with gr.Column(elem_id="app-wrap"):
|
| 282 |
+
notice_html = gr.HTML(f"<div class='notice'>{I18N['notice']['en']}</div>")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
|
| 284 |
with gr.Row():
|
| 285 |
with gr.Column(scale=1):
|
| 286 |
+
input_image = gr.Image(label=I18N["input_image"]["en"], type="pil", height=420)
|
| 287 |
|
| 288 |
with gr.Column(scale=1, elem_classes=["card"]):
|
| 289 |
dropdown = gr.Dropdown(
|
| 290 |
+
label=I18N["dropdown_label"]["en"],
|
| 291 |
+
choices=build_dropdown_choices("en"),
|
| 292 |
+
value=CAMERA_OPTIONS[0]["cn"],
|
| 293 |
+
allow_custom_value=False,
|
| 294 |
interactive=True,
|
| 295 |
)
|
| 296 |
|
| 297 |
+
custom_cn = gr.Textbox(
|
| 298 |
+
label=I18N["custom_cn_label"]["en"],
|
| 299 |
+
placeholder=I18N["custom_cn_ph"]["en"],
|
| 300 |
visible=False,
|
| 301 |
lines=2
|
| 302 |
)
|
| 303 |
|
| 304 |
extra_prompt = gr.Textbox(
|
| 305 |
+
label=I18N["extra_label"]["en"],
|
| 306 |
+
placeholder=I18N["extra_ph"]["en"],
|
| 307 |
lines=2
|
| 308 |
)
|
| 309 |
|
| 310 |
+
# 詳細設定アコーディオン
|
| 311 |
+
with gr.Accordion(I18N["accordion"]["en"], open=False) as adv_acc:
|
| 312 |
+
seed = gr.Slider(label=I18N["seed"]["en"], minimum=0, maximum=MAX_SEED, step=1, value=DEFAULT_SEED)
|
| 313 |
+
randomize_seed = gr.Checkbox(label=I18N["rand"]["en"], value=DEFAULT_RANDOMIZE)
|
| 314 |
+
true_guidance_scale = gr.Slider(label=I18N["tgs"]["en"], minimum=1.0, maximum=10.0, step=0.1, value=DEFAULT_TRUE_GUIDANCE_SCALE)
|
| 315 |
+
num_inference_steps = gr.Slider(label=I18N["steps"]["en"], minimum=1, maximum=40, step=1, value=DEFAULT_NUM_INFERENCE_STEPS)
|
|
|
|
| 316 |
|
| 317 |
+
run_button = gr.Button(I18N["run"]["en"], variant="primary")
|
| 318 |
|
| 319 |
+
selected_cn = gr.Textbox(label=I18N["sel_cn"]["en"], value=CAMERA_OPTIONS[0]["cn"], interactive=False)
|
| 320 |
+
final_prompt_preview = gr.Textbox(label=I18N["final_prev"]["en"], value="", interactive=False)
|
|
|
|
| 321 |
|
| 322 |
+
# 選択/入力のたびにプレビュー更新 & 自由入力欄の表示切替
|
| 323 |
+
def _sync(v_cn, extra, custom_text):
|
| 324 |
+
is_custom = (v_cn == CUSTOM_OPTION_VALUE)
|
| 325 |
+
base = (custom_text.strip() if is_custom else (v_cn or CAMERA_OPTIONS[0]["cn"]))
|
|
|
|
| 326 |
final = _append_prompt(base, extra) if base else ""
|
| 327 |
+
return base, final, gr.update(visible=is_custom)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
dropdown.change(
|
| 330 |
fn=_sync,
|
| 331 |
+
inputs=[dropdown, extra_prompt, custom_cn],
|
| 332 |
+
outputs=[selected_cn, final_prompt_preview, custom_cn]
|
| 333 |
)
|
| 334 |
extra_prompt.change(
|
| 335 |
fn=_sync,
|
| 336 |
+
inputs=[dropdown, extra_prompt, custom_cn],
|
| 337 |
+
outputs=[selected_cn, final_prompt_preview, custom_cn]
|
| 338 |
)
|
| 339 |
+
custom_cn.change(
|
| 340 |
fn=_sync,
|
| 341 |
+
inputs=[dropdown, extra_prompt, custom_cn],
|
| 342 |
+
outputs=[selected_cn, final_prompt_preview, custom_cn]
|
| 343 |
)
|
| 344 |
|
| 345 |
with gr.Row():
|
| 346 |
with gr.Column(scale=1, elem_classes=["card"]):
|
| 347 |
+
result_image = gr.Image(label=I18N["output"]["en"], type="pil", format="png", height=520, show_download_button=True)
|
| 348 |
+
status_text = gr.Textbox(label=I18N["status"]["en"], interactive=False)
|
| 349 |
+
sent_hdr_md = gr.Markdown(I18N["sent_hdr"]["en"])
|
|
|
|
| 350 |
final_prompt_small = gr.Textbox(show_label=False, interactive=False, elem_classes=["preview", "small"])
|
| 351 |
|
| 352 |
+
# 言語切替の実装
|
| 353 |
+
def _switch_lang(lang, current_dropdown_value):
|
| 354 |
+
# ラベル/見出し/プレースホルダの更新 + ドロップダンのchoicesの再構築
|
| 355 |
+
return (
|
| 356 |
+
gr.update(label=I18N["lang_label"][lang]), # lang_selector label
|
| 357 |
+
I18N["title"][lang], # title_md value
|
| 358 |
+
gr.update(value=f"<div class='notice'>{I18N['notice'][lang]}</div>"), # notice_html
|
| 359 |
+
gr.update(label=I18N["input_image"][lang]), # input_image label
|
| 360 |
+
gr.update(label=I18N["dropdown_label"][lang],
|
| 361 |
+
choices=build_dropdown_choices(lang),
|
| 362 |
+
value=current_dropdown_value if current_dropdown_value else CAMERA_OPTIONS[0]["cn"]), # dropdown
|
| 363 |
+
gr.update(label=I18N["custom_cn_label"][lang], placeholder=I18N["custom_cn_ph"][lang]), # custom_cn
|
| 364 |
+
gr.update(label=I18N["extra_label"][lang], placeholder=I18N["extra_ph"][lang]), # extra_prompt
|
| 365 |
+
gr.update(label=I18N["seed"][lang]), # seed
|
| 366 |
+
gr.update(label=I18N["rand"][lang]), # randomize_seed
|
| 367 |
+
gr.update(label=I18N["tgs"][lang]), # true_guidance_scale
|
| 368 |
+
gr.update(label=I18N["steps"][lang]), # num_inference_steps
|
| 369 |
+
gr.update(value=I18N["run"][lang]), # run_button text
|
| 370 |
+
gr.update(label=I18N["sel_cn"][lang]), # selected_cn
|
| 371 |
+
gr.update(label=I18N["final_prev"][lang]), # final_prompt_preview
|
| 372 |
+
gr.update(label=I18N["output"][lang]), # result_image
|
| 373 |
+
gr.update(label=I18N["status"][lang]), # status_text
|
| 374 |
+
I18N["sent_hdr"][lang], # sent_hdr_md
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
lang_selector.change(
|
| 378 |
+
fn=_switch_lang,
|
| 379 |
+
inputs=[lang_selector, dropdown],
|
| 380 |
+
outputs=[
|
| 381 |
+
lang_selector, # label update
|
| 382 |
+
title_md, # markdown title
|
| 383 |
+
notice_html, # notice
|
| 384 |
+
input_image, # image label
|
| 385 |
+
dropdown, # dropdown (choices/label/value)
|
| 386 |
+
custom_cn, # custom label/ph
|
| 387 |
+
extra_prompt, # extra label/ph
|
| 388 |
+
seed, # seed label
|
| 389 |
+
randomize_seed, # randomize label
|
| 390 |
+
true_guidance_scale, # tgs label
|
| 391 |
+
num_inference_steps, # steps label
|
| 392 |
+
run_button, # button text
|
| 393 |
+
selected_cn, # label
|
| 394 |
+
final_prompt_preview, # label
|
| 395 |
+
result_image, # label
|
| 396 |
+
status_text, # label
|
| 397 |
+
sent_hdr_md, # markdown content
|
| 398 |
+
],
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
# 実行:アコーディオンの値も渡す + 言語も渡す
|
| 402 |
run_button.click(
|
| 403 |
fn=generate_from_dropdown,
|
| 404 |
+
inputs=[input_image, dropdown, custom_cn, extra_prompt, seed, randomize_seed, true_guidance_scale, num_inference_steps, lang_selector],
|
| 405 |
+
outputs=[result_image, status_text, final_prompt_small],
|
| 406 |
)
|
| 407 |
|
| 408 |
if __name__ == "__main__":
|