seawolf2357 commited on
Commit
d2b37bf
ยท
verified ยท
1 Parent(s): ed24b25

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +585 -200
app.py CHANGED
@@ -6,10 +6,8 @@ import spaces
6
 
7
  from PIL import Image
8
  from diffusers import FlowMatchEulerDiscreteScheduler
9
- # from optimization import optimize_pipeline_
10
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
11
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
12
- # from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
13
 
14
  import math
15
  from huggingface_hub import hf_hub_download
@@ -50,15 +48,6 @@ pipe.unload_lora_weights()
50
 
51
  spaces.aoti_blocks_load(pipe.transformer, "zerogpu-aoti/Qwen-Image", variant="fa3")
52
 
53
- #pipe.transformer.__class__ = QwenImageTransformer2DModel
54
- #pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
55
-
56
- #optimize_pipeline_(
57
- # pipe,
58
- # image=[Image.new("RGB", (1024, 1024)), Image.new("RGB", (1024, 1024))],
59
- # prompt="prompt"
60
- #)
61
-
62
  MAX_SEED = np.iinfo(np.int32).max
63
 
64
 
@@ -71,25 +60,6 @@ def _generate_video_segment(
71
  """
72
  Generate a single video segment between two frames by calling an external
73
  Wan 2.2 image-to-video service hosted on Hugging Face Spaces.
74
-
75
- This helper function is used internally when the user asks to create
76
- a video between the input and output images.
77
-
78
- Args:
79
- input_image_path (str):
80
- Path to the starting frame image on disk.
81
- output_image_path (str):
82
- Path to the ending frame image on disk.
83
- prompt (str):
84
- Text prompt describing the camera movement / transition.
85
- request (gr.Request):
86
- Gradio request object, used here to forward the `x-ip-token`
87
- header to the downstream Space for authentication/rate limiting.
88
-
89
- Returns:
90
- str:
91
- A string returned by the external service, usually a URL or path
92
- to the generated video.
93
  """
94
  x_ip_token = request.headers['x-ip-token']
95
  video_client = Client(
@@ -113,28 +83,6 @@ def build_camera_prompt(
113
  ) -> str:
114
  """
115
  Build a camera movement prompt based on the chosen controls.
116
-
117
- This converts the provided control values into a prompt instruction with the corresponding trigger words for the multiple-angles LoRA.
118
-
119
- Args:
120
- rotate_deg (float, optional):
121
- Horizontal rotation in degrees. Positive values rotate left,
122
- negative values rotate right. Defaults to 0.0.
123
- move_forward (float, optional):
124
- Forward movement / zoom factor. Larger values imply moving the
125
- camera closer or into a close-up. Defaults to 0.0.
126
- vertical_tilt (float, optional):
127
- Vertical angle of the camera:
128
- - Negative โ‰ˆ bird's-eye view
129
- - Positive โ‰ˆ worm's-eye view
130
- Defaults to 0.0.
131
- wideangle (bool, optional):
132
- Whether to switch to a wide-angle lens style. Defaults to False.
133
-
134
- Returns:
135
- str:
136
- A text prompt describing the camera motion. If no controls are
137
- active, returns `"no camera movement"`.
138
  """
139
  prompt_parts = []
140
 
@@ -186,53 +134,7 @@ def infer_camera_edit(
186
  prev_output: Optional[Image.Image] = None,
187
  ) -> Tuple[Image.Image, int, str]:
188
  """
189
- Edit the camera angles/view of an image with Qwen Image Edit 2509 and dx8152's Qwen-Edit-2509-Multiple-angles LoRA.
190
-
191
- Applies a camera-style transformation (rotation, zoom, tilt, lens)
192
- to an input image.
193
-
194
- Args:
195
- image (PIL.Image.Image | None, optional):
196
- Input image to edit. If `None`, the function will instead try to
197
- use `prev_output`. At least one of `image` or `prev_output` must
198
- be available. Defaults to None.
199
- rotate_deg (float, optional):
200
- Horizontal rotation in degrees (-90, -45, 0, 45, 90). Positive values rotate
201
- to the left, negative to the right. Defaults to 0.0.
202
- move_forward (float, optional):
203
- Forward movement / zoom factor (0, 5, 10). Higher values move the
204
- camera closer; values >5 switch to a close-up style. Defaults to 0.0.
205
- vertical_tilt (float, optional):
206
- Vertical tilt (-1 to 1). -1 โ‰ˆ bird's-eye view, +1 โ‰ˆ worm's-eye view.
207
- Defaults to 0.0.
208
- wideangle (bool, optional):
209
- Whether to use a wide-angle lens style. Defaults to False.
210
- seed (int, optional):
211
- Random seed for the generation. Ignored if `randomize_seed=True`.
212
- Defaults to 0.
213
- randomize_seed (bool, optional):
214
- If True, a random seed (0..MAX_SEED) is chosen per call.
215
- Defaults to True.
216
- true_guidance_scale (float, optional):
217
- CFG / guidance scale controlling prompt adherence.
218
- Defaults to 1.0 since the demo is using a distilled transformer for faster inference.
219
- num_inference_steps (int, optional):
220
- Number of inference steps. Defaults to 4.
221
- height (int, optional):
222
- Output image height. Must typically be a multiple of 8.
223
- If set to 0, the model will infer a size. Defaults to 1024 if none is provided.
224
- width (int, optional):
225
- Output image width. Must typically be a multiple of 8.
226
- If set to 0, the model will infer a size. Defaults to 1024 if none is provided.
227
- prev_output (PIL.Image.Image | None, optional):
228
- Previous output image to use as input when no new image is uploaded.
229
- Defaults to None.
230
-
231
- Returns:
232
- Tuple[PIL.Image.Image, int, str]:
233
- - The edited output image.
234
- - The actual seed used for generation.
235
- - The constructed camera prompt string.
236
  """
237
  progress = gr.Progress(track_tqdm=True)
238
 
@@ -280,27 +182,7 @@ def create_video_between_images(
280
  request: gr.Request
281
  ) -> str:
282
  """
283
- Create a short transition video between the input and output images via the
284
- Wan 2.2 first-last-frame Space.
285
-
286
- Args:
287
- input_image (PIL.Image.Image | None):
288
- Starting frame image (the original / previous view).
289
- output_image (numpy.ndarray | None):
290
- Ending frame image - the output image with the the edited camera angles.
291
- prompt (str):
292
- The camera movement prompt used to describe the transition.
293
- request (gr.Request):
294
- Gradio request object, used to forward the `x-ip-token` header
295
- to the video generation app.
296
-
297
- Returns:
298
- str:
299
- a path pointing to the generated video.
300
-
301
- Raises:
302
- gr.Error:
303
- If either image is missing or if the video generation fails.
304
  """
305
  if input_image is None or output_image is None:
306
  raise gr.Error("Both input and output images are required to create a video.")
@@ -326,41 +208,504 @@ def create_video_between_images(
326
  raise gr.Error(f"Video generation failed: {e}")
327
 
328
 
329
- # --- UI ---
330
- css = '''#col-container { max-width: 800px; margin: 0 auto; }
331
- .dark .progress-text{color: white !important}
332
- #examples{max-width: 800px; margin: 0 auto; }'''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
 
334
 
335
  def reset_all() -> list:
336
  """
337
  Reset all camera control knobs and flags to their default values.
338
-
339
- This is used by the "Reset" button to set:
340
- - rotate_deg = 0
341
- - move_forward = 0
342
- - vertical_tilt = 0
343
- - wideangle = False
344
- - is_reset = True
345
-
346
- Returns:
347
- list:
348
- A list of values matching the order of the reset outputs:
349
- [rotate_deg, move_forward, vertical_tilt, wideangle, is_reset, True]
350
  """
351
- return [0, 0, 0, 0, False, True]
352
 
353
 
354
  def end_reset() -> bool:
355
  """
356
  Mark the end of a reset cycle.
357
-
358
- This helper is chained after `reset_all` to set the internal
359
- `is_reset` flag back to False, so that live inference can resume.
360
-
361
- Returns:
362
- bool:
363
- Always returns False.
364
  """
365
  return False
366
 
@@ -369,16 +714,7 @@ def update_dimensions_on_upload(
369
  image: Optional[Image.Image]
370
  ) -> Tuple[int, int]:
371
  """
372
- Compute recommended (width, height) for the output resolution when an
373
- image is uploaded while preserveing the aspect ratio.
374
-
375
- Args:
376
- image (PIL.Image.Image | None):
377
- The uploaded image. If `None`, defaults to (1024, 1024).
378
-
379
- Returns:
380
- Tuple[int, int]:
381
- The new (width, height).
382
  """
383
  if image is None:
384
  return 1024, 1024
@@ -401,99 +737,160 @@ def update_dimensions_on_upload(
401
  return new_width, new_height
402
 
403
 
404
- with gr.Blocks() as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  with gr.Column(elem_id="col-container"):
406
- gr.Markdown("## ๐ŸŽฌ Qwen Image Edit โ€” Camera Angle Control")
407
-
408
-
409
- with gr.Row():
410
- with gr.Column():
411
- image = gr.Image(label="Input Image", type="pil")
 
 
412
  prev_output = gr.Image(value=None, visible=False)
413
  is_reset = gr.Checkbox(value=False, visible=False)
414
-
415
- with gr.Tab("Camera Controls"):
416
  rotate_deg = gr.Slider(
417
- label="Rotate Right-Left (degrees ยฐ)",
418
  minimum=-90,
419
  maximum=90,
420
  step=45,
421
  value=0
422
  )
423
  move_forward = gr.Slider(
424
- label="Move Forward โ†’ Close-Up",
425
  minimum=0,
426
  maximum=10,
427
  step=5,
428
  value=0
429
  )
430
  vertical_tilt = gr.Slider(
431
- label="Vertical Angle (Bird โ†” Worm)",
432
  minimum=-1,
433
  maximum=1,
434
  step=1,
435
  value=0
436
  )
437
- wideangle = gr.Checkbox(label="Wide-Angle Lens", value=False)
 
 
 
 
438
  with gr.Row():
439
- reset_btn = gr.Button("Reset")
440
- run_btn = gr.Button("Generate", variant="primary")
441
-
442
- with gr.Accordion("Advanced Settings", open=False):
 
 
 
 
 
 
 
 
 
443
  seed = gr.Slider(
444
- label="Seed",
445
  minimum=0,
446
  maximum=MAX_SEED,
447
  step=1,
448
  value=0
449
  )
450
  randomize_seed = gr.Checkbox(
451
- label="Randomize Seed",
452
  value=True
453
  )
454
  true_guidance_scale = gr.Slider(
455
- label="True Guidance Scale",
456
  minimum=1.0,
457
  maximum=10.0,
458
  step=0.1,
459
  value=1.0
460
  )
461
  num_inference_steps = gr.Slider(
462
- label="Inference Steps",
463
  minimum=1,
464
  maximum=40,
465
  step=1,
466
  value=20
467
  )
468
  height = gr.Slider(
469
- label="Height",
470
  minimum=256,
471
  maximum=2048,
472
  step=8,
473
  value=1024
474
  )
475
  width = gr.Slider(
476
- label="Width",
477
  minimum=256,
478
  maximum=2048,
479
  step=8,
480
  value=1024
481
  )
482
-
483
- with gr.Column():
484
- result = gr.Image(label="Output Image", interactive=False)
485
- prompt_preview = gr.Textbox(label="Processed Prompt", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
486
  create_video_button = gr.Button(
487
- "๐ŸŽฅ Create Video Between Images",
488
  variant="secondary",
489
- visible=False
 
490
  )
 
491
  with gr.Group(visible=False) as video_group:
492
  video_output = gr.Video(
493
- label="Generated Video",
494
- buttons=["download"],
495
  autoplay=True
496
  )
 
 
 
 
 
 
 
 
497
 
498
  inputs = [
499
  image, rotate_deg, move_forward,
@@ -513,22 +910,10 @@ with gr.Blocks() as demo:
513
  # Manual generation with video button visibility control
514
  def infer_and_show_video_button(*args: Any):
515
  """
516
- Wrapper around `infer_camera_edit` that also controls the visibility
517
  of the 'Create Video Between Images' button.
518
-
519
- The first argument in `args` is expected to be the input image; if both
520
- input and output images are present, the video button is shown.
521
-
522
- Args:
523
- *args:
524
- Positional arguments forwarded directly to `infer_camera_edit`.
525
-
526
- Returns:
527
- tuple:
528
- (output_image, seed, prompt, video_button_visibility_update)
529
  """
530
  result_img, result_seed, result_prompt = infer_camera_edit(*args)
531
- # Show video button if we have both input and output images
532
  show_button = args[0] is not None and result_img is not None
533
  return result_img, result_seed, result_prompt, gr.update(visible=show_button)
534
 
@@ -598,7 +983,6 @@ with gr.Blocks() as demo:
598
  return gr.update(), gr.update(), gr.update(), gr.update()
599
  else:
600
  result_img, result_seed, result_prompt = infer_camera_edit(*args)
601
- # Show video button if we have both input and output
602
  show_button = args[0] is not None and result_img is not None
603
  return result_img, result_seed, result_prompt, gr.update(visible=show_button)
604
 
@@ -627,4 +1011,5 @@ with gr.Blocks() as demo:
627
  gr.api(infer_camera_edit, api_name="infer_edit_camera_angles")
628
  gr.api(create_video_between_images, api_name="create_video_between_images")
629
 
630
- demo.launch(mcp_server=True, theme=gr.themes.Citrus(), css=css, footer_links=["api", "gradio", "settings"])
 
 
6
 
7
  from PIL import Image
8
  from diffusers import FlowMatchEulerDiscreteScheduler
 
9
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
10
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
 
11
 
12
  import math
13
  from huggingface_hub import hf_hub_download
 
48
 
49
  spaces.aoti_blocks_load(pipe.transformer, "zerogpu-aoti/Qwen-Image", variant="fa3")
50
 
 
 
 
 
 
 
 
 
 
51
  MAX_SEED = np.iinfo(np.int32).max
52
 
53
 
 
60
  """
61
  Generate a single video segment between two frames by calling an external
62
  Wan 2.2 image-to-video service hosted on Hugging Face Spaces.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  """
64
  x_ip_token = request.headers['x-ip-token']
65
  video_client = Client(
 
83
  ) -> str:
84
  """
85
  Build a camera movement prompt based on the chosen controls.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  """
87
  prompt_parts = []
88
 
 
134
  prev_output: Optional[Image.Image] = None,
135
  ) -> Tuple[Image.Image, int, str]:
136
  """
137
+ Edit the camera angles/view of an image with Qwen Image Edit 2509.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  """
139
  progress = gr.Progress(track_tqdm=True)
140
 
 
182
  request: gr.Request
183
  ) -> str:
184
  """
185
+ Create a short transition video between the input and output images.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  """
187
  if input_image is None or output_image is None:
188
  raise gr.Error("Both input and output images are required to create a video.")
 
208
  raise gr.Error(f"Video generation failed: {e}")
209
 
210
 
211
+ # ============================================
212
+ # ๐ŸŽจ Comic Classic Theme - Toon Playground
213
+ # ============================================
214
+
215
+ css = """
216
+ /* ===== ๐ŸŽจ Google Fonts Import ===== */
217
+ @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
218
+
219
+ /* ===== ๐ŸŽจ Comic Classic ๋ฐฐ๊ฒฝ - ๋นˆํ‹ฐ์ง€ ํŽ˜์ดํผ + ๋„ํŠธ ํŒจํ„ด ===== */
220
+ .gradio-container {
221
+ background-color: #FEF9C3 !important;
222
+ background-image:
223
+ radial-gradient(#1F2937 1px, transparent 1px) !important;
224
+ background-size: 20px 20px !important;
225
+ min-height: 100vh !important;
226
+ font-family: 'Comic Neue', cursive, sans-serif !important;
227
+ }
228
+
229
+ /* ===== ํ—ˆ๊น…ํŽ˜์ด์Šค ์ƒ๋‹จ ์š”์†Œ ์ˆจ๊น€ ===== */
230
+ .huggingface-space-header,
231
+ #space-header,
232
+ .space-header,
233
+ [class*="space-header"],
234
+ .svelte-1ed2p3z,
235
+ .space-header-badge,
236
+ .header-badge,
237
+ [data-testid="space-header"],
238
+ .svelte-kqij2n,
239
+ .svelte-1ax1toq,
240
+ .embed-container > div:first-child {
241
+ display: none !important;
242
+ visibility: hidden !important;
243
+ height: 0 !important;
244
+ width: 0 !important;
245
+ overflow: hidden !important;
246
+ opacity: 0 !important;
247
+ pointer-events: none !important;
248
+ }
249
+
250
+ /* ===== Footer ์™„์ „ ์ˆจ๊น€ ===== */
251
+ footer,
252
+ .footer,
253
+ .gradio-container footer,
254
+ .built-with,
255
+ [class*="footer"],
256
+ .gradio-footer,
257
+ .main-footer,
258
+ div[class*="footer"],
259
+ .show-api,
260
+ .built-with-gradio,
261
+ a[href*="gradio.app"],
262
+ a[href*="huggingface.co/spaces"] {
263
+ display: none !important;
264
+ visibility: hidden !important;
265
+ height: 0 !important;
266
+ padding: 0 !important;
267
+ margin: 0 !important;
268
+ }
269
+
270
+ /* ===== ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ ===== */
271
+ #col-container {
272
+ max-width: 1000px;
273
+ margin: 0 auto;
274
+ }
275
+
276
+ /* ===== ๐ŸŽจ ํ—ค๋” ํƒ€์ดํ‹€ - ์ฝ”๋ฏน ์Šคํƒ€์ผ ===== */
277
+ .header-text h1 {
278
+ font-family: 'Bangers', cursive !important;
279
+ color: #1F2937 !important;
280
+ font-size: 3.5rem !important;
281
+ font-weight: 400 !important;
282
+ text-align: center !important;
283
+ margin-bottom: 0.5rem !important;
284
+ text-shadow:
285
+ 4px 4px 0px #FACC15,
286
+ 6px 6px 0px #1F2937 !important;
287
+ letter-spacing: 3px !important;
288
+ -webkit-text-stroke: 2px #1F2937 !important;
289
+ }
290
+
291
+ /* ===== ๐ŸŽจ ์„œ๋ธŒํƒ€์ดํ‹€ ===== */
292
+ .subtitle {
293
+ text-align: center !important;
294
+ font-family: 'Comic Neue', cursive !important;
295
+ font-size: 1.2rem !important;
296
+ color: #1F2937 !important;
297
+ margin-bottom: 1.5rem !important;
298
+ font-weight: 700 !important;
299
+ }
300
+
301
+ /* ===== ๐ŸŽจ ์นด๋“œ/ํŒจ๋„ - ๋งŒํ™” ํ”„๋ ˆ์ž„ ์Šคํƒ€์ผ ===== */
302
+ .gr-panel,
303
+ .gr-box,
304
+ .gr-form,
305
+ .block,
306
+ .gr-group {
307
+ background: #FFFFFF !important;
308
+ border: 3px solid #1F2937 !important;
309
+ border-radius: 8px !important;
310
+ box-shadow: 6px 6px 0px #1F2937 !important;
311
+ transition: all 0.2s ease !important;
312
+ }
313
+
314
+ .gr-panel:hover,
315
+ .block:hover {
316
+ transform: translate(-2px, -2px) !important;
317
+ box-shadow: 8px 8px 0px #1F2937 !important;
318
+ }
319
+
320
+ /* ===== ๐ŸŽจ ์ž…๋ ฅ ํ•„๋“œ (Textbox) ===== */
321
+ textarea,
322
+ input[type="text"],
323
+ input[type="number"] {
324
+ background: #FFFFFF !important;
325
+ border: 3px solid #1F2937 !important;
326
+ border-radius: 8px !important;
327
+ color: #1F2937 !important;
328
+ font-family: 'Comic Neue', cursive !important;
329
+ font-size: 1rem !important;
330
+ font-weight: 700 !important;
331
+ transition: all 0.2s ease !important;
332
+ }
333
+
334
+ textarea:focus,
335
+ input[type="text"]:focus,
336
+ input[type="number"]:focus {
337
+ border-color: #3B82F6 !important;
338
+ box-shadow: 4px 4px 0px #3B82F6 !important;
339
+ outline: none !important;
340
+ }
341
+
342
+ textarea::placeholder {
343
+ color: #9CA3AF !important;
344
+ font-weight: 400 !important;
345
+ }
346
+
347
+ /* ===== ๐ŸŽจ Primary ๋ฒ„ํŠผ - ์ฝ”๋ฏน ๋ธ”๋ฃจ ===== */
348
+ .gr-button-primary,
349
+ button.primary,
350
+ .gr-button.primary {
351
+ background: #3B82F6 !important;
352
+ border: 3px solid #1F2937 !important;
353
+ border-radius: 8px !important;
354
+ color: #FFFFFF !important;
355
+ font-family: 'Bangers', cursive !important;
356
+ font-weight: 400 !important;
357
+ font-size: 1.3rem !important;
358
+ letter-spacing: 2px !important;
359
+ padding: 14px 28px !important;
360
+ box-shadow: 5px 5px 0px #1F2937 !important;
361
+ transition: all 0.1s ease !important;
362
+ text-shadow: 1px 1px 0px #1F2937 !important;
363
+ }
364
+
365
+ .gr-button-primary:hover,
366
+ button.primary:hover,
367
+ .gr-button.primary:hover {
368
+ background: #2563EB !important;
369
+ transform: translate(-2px, -2px) !important;
370
+ box-shadow: 7px 7px 0px #1F2937 !important;
371
+ }
372
+
373
+ .gr-button-primary:active,
374
+ button.primary:active,
375
+ .gr-button.primary:active {
376
+ transform: translate(3px, 3px) !important;
377
+ box-shadow: 2px 2px 0px #1F2937 !important;
378
+ }
379
+
380
+ /* ===== ๐ŸŽจ Secondary ๋ฒ„ํŠผ - ์ฝ”๋ฏน ๋ ˆ๋“œ ===== */
381
+ .gr-button-secondary,
382
+ button.secondary,
383
+ .generate-btn {
384
+ background: #EF4444 !important;
385
+ border: 3px solid #1F2937 !important;
386
+ border-radius: 8px !important;
387
+ color: #FFFFFF !important;
388
+ font-family: 'Bangers', cursive !important;
389
+ font-weight: 400 !important;
390
+ font-size: 1.1rem !important;
391
+ letter-spacing: 1px !important;
392
+ box-shadow: 4px 4px 0px #1F2937 !important;
393
+ transition: all 0.1s ease !important;
394
+ text-shadow: 1px 1px 0px #1F2937 !important;
395
+ }
396
+
397
+ .gr-button-secondary:hover,
398
+ button.secondary:hover,
399
+ .generate-btn:hover {
400
+ background: #DC2626 !important;
401
+ transform: translate(-2px, -2px) !important;
402
+ box-shadow: 6px 6px 0px #1F2937 !important;
403
+ }
404
+
405
+ .gr-button-secondary:active,
406
+ button.secondary:active,
407
+ .generate-btn:active {
408
+ transform: translate(2px, 2px) !important;
409
+ box-shadow: 2px 2px 0px #1F2937 !important;
410
+ }
411
+
412
+ /* ===== ๐ŸŽจ Reset ๋ฒ„ํŠผ - ์ฝ”๋ฏน ์˜๋กœ์šฐ ===== */
413
+ .reset-btn {
414
+ background: #FACC15 !important;
415
+ border: 3px solid #1F2937 !important;
416
+ border-radius: 8px !important;
417
+ color: #1F2937 !important;
418
+ font-family: 'Bangers', cursive !important;
419
+ font-weight: 400 !important;
420
+ font-size: 1.1rem !important;
421
+ letter-spacing: 1px !important;
422
+ box-shadow: 4px 4px 0px #1F2937 !important;
423
+ transition: all 0.1s ease !important;
424
+ }
425
+
426
+ .reset-btn:hover {
427
+ background: #EAB308 !important;
428
+ transform: translate(-2px, -2px) !important;
429
+ box-shadow: 6px 6px 0px #1F2937 !important;
430
+ }
431
+
432
+ .reset-btn:active {
433
+ transform: translate(2px, 2px) !important;
434
+ box-shadow: 2px 2px 0px #1F2937 !important;
435
+ }
436
+
437
+ /* ===== ๐ŸŽจ Video ๋ฒ„ํŠผ - ์ฝ”๋ฏน ํผํ”Œ ===== */
438
+ .video-btn {
439
+ background: #8B5CF6 !important;
440
+ border: 3px solid #1F2937 !important;
441
+ border-radius: 8px !important;
442
+ color: #FFFFFF !important;
443
+ font-family: 'Bangers', cursive !important;
444
+ font-weight: 400 !important;
445
+ font-size: 1.1rem !important;
446
+ letter-spacing: 1px !important;
447
+ box-shadow: 4px 4px 0px #1F2937 !important;
448
+ transition: all 0.1s ease !important;
449
+ text-shadow: 1px 1px 0px #1F2937 !important;
450
+ }
451
+
452
+ .video-btn:hover {
453
+ background: #7C3AED !important;
454
+ transform: translate(-2px, -2px) !important;
455
+ box-shadow: 6px 6px 0px #1F2937 !important;
456
+ }
457
+
458
+ .video-btn:active {
459
+ transform: translate(2px, 2px) !important;
460
+ box-shadow: 2px 2px 0px #1F2937 !important;
461
+ }
462
+
463
+ /* ===== ๐ŸŽจ ๋กœ๊ทธ ์ถœ๋ ฅ ์˜์—ญ ===== */
464
+ .info-log textarea,
465
+ .prompt-preview textarea {
466
+ background: #1F2937 !important;
467
+ color: #10B981 !important;
468
+ font-family: 'Courier New', monospace !important;
469
+ font-size: 0.9rem !important;
470
+ font-weight: 400 !important;
471
+ border: 3px solid #10B981 !important;
472
+ border-radius: 8px !important;
473
+ box-shadow: 4px 4px 0px #10B981 !important;
474
+ }
475
+
476
+ /* ===== ๐ŸŽจ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์˜์—ญ ===== */
477
+ .image-upload {
478
+ border: 4px dashed #3B82F6 !important;
479
+ border-radius: 12px !important;
480
+ background: #EFF6FF !important;
481
+ transition: all 0.2s ease !important;
482
+ }
483
+
484
+ .image-upload:hover {
485
+ border-color: #EF4444 !important;
486
+ background: #FEF2F2 !important;
487
+ }
488
+
489
+ /* ===== ๐ŸŽจ ์•„์ฝ”๋””์–ธ - ๋งํ’์„  ์Šคํƒ€์ผ ===== */
490
+ .gr-accordion {
491
+ background: #FACC15 !important;
492
+ border: 3px solid #1F2937 !important;
493
+ border-radius: 8px !important;
494
+ box-shadow: 4px 4px 0px #1F2937 !important;
495
+ }
496
+
497
+ .gr-accordion-header {
498
+ color: #1F2937 !important;
499
+ font-family: 'Comic Neue', cursive !important;
500
+ font-weight: 700 !important;
501
+ font-size: 1.1rem !important;
502
+ }
503
+
504
+ /* ===== ๐ŸŽจ ํƒญ ์Šคํƒ€์ผ - ์ฝ”๋ฏน ์Šคํƒ€์ผ ===== */
505
+ .gr-tab-nav {
506
+ background: #FFFFFF !important;
507
+ border: 3px solid #1F2937 !important;
508
+ border-radius: 8px 8px 0 0 !important;
509
+ box-shadow: 4px 0px 0px #1F2937 !important;
510
+ }
511
+
512
+ .gr-tab-nav button {
513
+ font-family: 'Comic Neue', cursive !important;
514
+ font-weight: 700 !important;
515
+ color: #1F2937 !important;
516
+ border: none !important;
517
+ background: transparent !important;
518
+ }
519
+
520
+ .gr-tab-nav button.selected {
521
+ background: #3B82F6 !important;
522
+ color: #FFFFFF !important;
523
+ border-radius: 6px !important;
524
+ }
525
+
526
+ /* ===== ๐ŸŽจ ์Šฌ๋ผ์ด๋” ์Šคํƒ€์ผ ===== */
527
+ input[type="range"] {
528
+ accent-color: #3B82F6 !important;
529
+ }
530
+
531
+ .gr-slider {
532
+ background: #FFFFFF !important;
533
+ }
534
+
535
+ /* ===== ๐ŸŽจ ์ฒดํฌ๋ฐ•์Šค ์Šคํƒ€์ผ ===== */
536
+ input[type="checkbox"] {
537
+ accent-color: #3B82F6 !important;
538
+ width: 20px !important;
539
+ height: 20px !important;
540
+ border: 2px solid #1F2937 !important;
541
+ border-radius: 4px !important;
542
+ }
543
+
544
+ /* ===== ๐ŸŽจ ์ด๋ฏธ์ง€ ์ถœ๋ ฅ ์˜์—ญ ===== */
545
+ .gr-image,
546
+ .image-container {
547
+ border: 4px solid #1F2937 !important;
548
+ border-radius: 8px !important;
549
+ box-shadow: 8px 8px 0px #1F2937 !important;
550
+ overflow: hidden !important;
551
+ background: #FFFFFF !important;
552
+ }
553
+
554
+ /* ===== ๐ŸŽจ ๋น„๋””์˜ค ์ถœ๋ ฅ ์˜์—ญ ===== */
555
+ .gr-video {
556
+ border: 4px solid #1F2937 !important;
557
+ border-radius: 8px !important;
558
+ box-shadow: 8px 8px 0px #8B5CF6 !important;
559
+ overflow: hidden !important;
560
+ background: #FFFFFF !important;
561
+ }
562
+
563
+ /* ===== ๐ŸŽจ ๋ผ๋ฒจ ์Šคํƒ€์ผ ===== */
564
+ label,
565
+ .gr-input-label,
566
+ .gr-block-label {
567
+ color: #1F2937 !important;
568
+ font-family: 'Comic Neue', cursive !important;
569
+ font-weight: 700 !important;
570
+ font-size: 1rem !important;
571
+ }
572
+
573
+ span.gr-label {
574
+ color: #1F2937 !important;
575
+ }
576
+
577
+ /* ===== ๐ŸŽจ ์ •๋ณด ํ…์ŠคํŠธ ===== */
578
+ .gr-info,
579
+ .info {
580
+ color: #6B7280 !important;
581
+ font-family: 'Comic Neue', cursive !important;
582
+ font-size: 0.9rem !important;
583
+ }
584
+
585
+ /* ===== ๐ŸŽจ ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ===== */
586
+ .progress-bar,
587
+ .gr-progress-bar {
588
+ background: #3B82F6 !important;
589
+ border: 2px solid #1F2937 !important;
590
+ border-radius: 4px !important;
591
+ }
592
+
593
+ .progress-text {
594
+ color: #1F2937 !important;
595
+ font-family: 'Comic Neue', cursive !important;
596
+ font-weight: 700 !important;
597
+ }
598
+
599
+ /* ===== ๐ŸŽจ ์Šคํฌ๋กค๋ฐ” - ์ฝ”๋ฏน ์Šคํƒ€์ผ ===== */
600
+ ::-webkit-scrollbar {
601
+ width: 12px;
602
+ height: 12px;
603
+ }
604
+
605
+ ::-webkit-scrollbar-track {
606
+ background: #FEF9C3;
607
+ border: 2px solid #1F2937;
608
+ }
609
+
610
+ ::-webkit-scrollbar-thumb {
611
+ background: #3B82F6;
612
+ border: 2px solid #1F2937;
613
+ border-radius: 0px;
614
+ }
615
+
616
+ ::-webkit-scrollbar-thumb:hover {
617
+ background: #EF4444;
618
+ }
619
+
620
+ /* ===== ๐ŸŽจ ์„ ํƒ ํ•˜์ด๋ผ์ดํŠธ ===== */
621
+ ::selection {
622
+ background: #FACC15;
623
+ color: #1F2937;
624
+ }
625
+
626
+ /* ===== ๐ŸŽจ ๋งํฌ ์Šคํƒ€์ผ ===== */
627
+ a {
628
+ color: #3B82F6 !important;
629
+ text-decoration: none !important;
630
+ font-weight: 700 !important;
631
+ }
632
+
633
+ a:hover {
634
+ color: #EF4444 !important;
635
+ }
636
+
637
+ /* ===== ๐ŸŽจ Row/Column ๊ฐ„๊ฒฉ ===== */
638
+ .gr-row {
639
+ gap: 1.5rem !important;
640
+ }
641
+
642
+ .gr-column {
643
+ gap: 1rem !important;
644
+ }
645
+
646
+ /* ===== ๐ŸŽจ Examples ์„น์…˜ ===== */
647
+ #examples {
648
+ max-width: 1000px;
649
+ margin: 0 auto;
650
+ }
651
+
652
+ #examples .gr-sample {
653
+ border: 3px solid #1F2937 !important;
654
+ border-radius: 8px !important;
655
+ box-shadow: 4px 4px 0px #1F2937 !important;
656
+ background: #FFFFFF !important;
657
+ transition: all 0.2s ease !important;
658
+ }
659
+
660
+ #examples .gr-sample:hover {
661
+ transform: translate(-2px, -2px) !important;
662
+ box-shadow: 6px 6px 0px #1F2937 !important;
663
+ }
664
+
665
+ /* ===== ๋ฐ˜์‘ํ˜• ์กฐ์ • ===== */
666
+ @media (max-width: 768px) {
667
+ .header-text h1 {
668
+ font-size: 2.2rem !important;
669
+ text-shadow:
670
+ 3px 3px 0px #FACC15,
671
+ 4px 4px 0px #1F2937 !important;
672
+ }
673
+
674
+ .gr-button-primary,
675
+ button.primary {
676
+ padding: 12px 20px !important;
677
+ font-size: 1.1rem !important;
678
+ }
679
+
680
+ .gr-panel,
681
+ .block {
682
+ box-shadow: 4px 4px 0px #1F2937 !important;
683
+ }
684
+ }
685
+
686
+ /* ===== ๐ŸŽจ ๋‹คํฌ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™” (์ฝ”๋ฏน์€ ๋ฐ์•„์•ผ ํ•จ) ===== */
687
+ @media (prefers-color-scheme: dark) {
688
+ .gradio-container {
689
+ background-color: #FEF9C3 !important;
690
+ }
691
+
692
+ .progress-text {
693
+ color: #1F2937 !important;
694
+ }
695
+ }
696
+ """
697
 
698
 
699
  def reset_all() -> list:
700
  """
701
  Reset all camera control knobs and flags to their default values.
 
 
 
 
 
 
 
 
 
 
 
 
702
  """
703
+ return [0, 0, 0, False, True]
704
 
705
 
706
  def end_reset() -> bool:
707
  """
708
  Mark the end of a reset cycle.
 
 
 
 
 
 
 
709
  """
710
  return False
711
 
 
714
  image: Optional[Image.Image]
715
  ) -> Tuple[int, int]:
716
  """
717
+ Compute recommended (width, height) for the output resolution.
 
 
 
 
 
 
 
 
 
718
  """
719
  if image is None:
720
  return 1024, 1024
 
737
  return new_width, new_height
738
 
739
 
740
+ # Build the Gradio interface with Comic Classic Theme
741
+ with gr.Blocks(fill_height=True, css=css) as demo:
742
+
743
+ # HOME Badge
744
+ gr.HTML("""
745
+ <div style="text-align: center; margin: 20px 0 10px 0;">
746
+ <a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
747
+ <img src="https://img.shields.io/static/v1?label=๐Ÿ  HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
748
+ </a>
749
+ </div>
750
+ """)
751
+
752
+ # Header Title
753
+ gr.Markdown(
754
+ """
755
+ # ๐ŸŽฌ CAMERA ANGLE CONTROL ๐Ÿ“ท
756
+ """,
757
+ elem_classes="header-text"
758
+ )
759
+
760
+ gr.Markdown(
761
+ """
762
+ <p class="subtitle">๐Ÿ–ผ๏ธ Upload an image and control camera angles with AI magic! โœจ</p>
763
+ """,
764
+ )
765
+
766
  with gr.Column(elem_id="col-container"):
767
+ with gr.Row(equal_height=False):
768
+ # Left column - Input
769
+ with gr.Column(scale=1, min_width=320):
770
+ image = gr.Image(
771
+ label="๐Ÿ“ท Upload Your Image",
772
+ type="pil",
773
+ elem_classes="image-upload"
774
+ )
775
  prev_output = gr.Image(value=None, visible=False)
776
  is_reset = gr.Checkbox(value=False, visible=False)
777
+
778
+ with gr.Accordion("๐ŸŽฎ Camera Controls", open=True):
779
  rotate_deg = gr.Slider(
780
+ label="โ†”๏ธ Rotate Right-Left (degrees ยฐ)",
781
  minimum=-90,
782
  maximum=90,
783
  step=45,
784
  value=0
785
  )
786
  move_forward = gr.Slider(
787
+ label="๐Ÿ” Move Forward โ†’ Close-Up",
788
  minimum=0,
789
  maximum=10,
790
  step=5,
791
  value=0
792
  )
793
  vertical_tilt = gr.Slider(
794
+ label="โ†•๏ธ Vertical Angle (Bird โ†” Worm)",
795
  minimum=-1,
796
  maximum=1,
797
  step=1,
798
  value=0
799
  )
800
+ wideangle = gr.Checkbox(
801
+ label="๐Ÿ“ Wide-Angle Lens",
802
+ value=False
803
+ )
804
+
805
  with gr.Row():
806
+ reset_btn = gr.Button(
807
+ "๐Ÿ”„ RESET",
808
+ variant="secondary",
809
+ elem_classes="reset-btn"
810
+ )
811
+ run_btn = gr.Button(
812
+ "๐ŸŽฌ GENERATE!",
813
+ variant="primary",
814
+ size="lg",
815
+ elem_classes="generate-btn"
816
+ )
817
+
818
+ with gr.Accordion("โš™๏ธ Advanced Settings", open=False):
819
  seed = gr.Slider(
820
+ label="๐ŸŽฒ Seed",
821
  minimum=0,
822
  maximum=MAX_SEED,
823
  step=1,
824
  value=0
825
  )
826
  randomize_seed = gr.Checkbox(
827
+ label="๐Ÿ”€ Randomize Seed",
828
  value=True
829
  )
830
  true_guidance_scale = gr.Slider(
831
+ label="๐Ÿ“Š True Guidance Scale",
832
  minimum=1.0,
833
  maximum=10.0,
834
  step=0.1,
835
  value=1.0
836
  )
837
  num_inference_steps = gr.Slider(
838
+ label="๐Ÿ”ข Inference Steps",
839
  minimum=1,
840
  maximum=40,
841
  step=1,
842
  value=20
843
  )
844
  height = gr.Slider(
845
+ label="๐Ÿ“ Height",
846
  minimum=256,
847
  maximum=2048,
848
  step=8,
849
  value=1024
850
  )
851
  width = gr.Slider(
852
+ label="๐Ÿ“ Width",
853
  minimum=256,
854
  maximum=2048,
855
  step=8,
856
  value=1024
857
  )
858
+
859
+ # Right column - Output
860
+ with gr.Column(scale=1, min_width=320):
861
+ result = gr.Image(
862
+ label="๐Ÿ–ผ๏ธ Output Image",
863
+ interactive=False,
864
+ height=400,
865
+ )
866
+
867
+ prompt_preview = gr.Textbox(
868
+ label="๐Ÿ“ Generated Prompt",
869
+ interactive=False,
870
+ lines=3,
871
+ elem_classes="prompt-preview"
872
+ )
873
+
874
  create_video_button = gr.Button(
875
+ "๐ŸŽฅ CREATE VIDEO BETWEEN IMAGES!",
876
  variant="secondary",
877
+ visible=False,
878
+ elem_classes="video-btn"
879
  )
880
+
881
  with gr.Group(visible=False) as video_group:
882
  video_output = gr.Video(
883
+ label="๐ŸŽฌ Generated Video",
 
884
  autoplay=True
885
  )
886
+
887
+ gr.Markdown(
888
+ """
889
+ <p style="text-align: center; margin-top: 10px; font-weight: 700; color: #1F2937;">
890
+ ๐Ÿ’ก Right-click on the image to save, or use the download button!
891
+ </p>
892
+ """
893
+ )
894
 
895
  inputs = [
896
  image, rotate_deg, move_forward,
 
910
  # Manual generation with video button visibility control
911
  def infer_and_show_video_button(*args: Any):
912
  """
913
+ Wrapper around infer_camera_edit that also controls the visibility
914
  of the 'Create Video Between Images' button.
 
 
 
 
 
 
 
 
 
 
 
915
  """
916
  result_img, result_seed, result_prompt = infer_camera_edit(*args)
 
917
  show_button = args[0] is not None and result_img is not None
918
  return result_img, result_seed, result_prompt, gr.update(visible=show_button)
919
 
 
983
  return gr.update(), gr.update(), gr.update(), gr.update()
984
  else:
985
  result_img, result_seed, result_prompt = infer_camera_edit(*args)
 
986
  show_button = args[0] is not None and result_img is not None
987
  return result_img, result_seed, result_prompt, gr.update(visible=show_button)
988
 
 
1011
  gr.api(infer_camera_edit, api_name="infer_edit_camera_angles")
1012
  gr.api(create_video_between_images, api_name="create_video_between_images")
1013
 
1014
+ if __name__ == "__main__":
1015
+ demo.launch(mcp_server=True)