ImageStudio Maintainer Claude Opus 4.8 (1M context) commited on
Commit
74bfa68
·
1 Parent(s): 52379d0

feat: add Qwen3.5-4B Prompt Assistant tab (text + image, single-turn)

Browse files

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +189 -77
app.py CHANGED
@@ -16,6 +16,7 @@ from diffusers import (
16
  )
17
  from compel import Compel, ReturnedEmbeddingsType
18
  from huggingface_hub import hf_hub_download
 
19
 
20
  import r2_uploader
21
 
@@ -107,6 +108,28 @@ noobxl_pipe.to("cuda")
107
 
108
  print("Pipelines loaded!")
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  # =============================================================================
112
  # Compel-based prompt weighting helpers (ported from the NoobXL11 reference
@@ -409,6 +432,50 @@ def _generate_image_inner(
409
  return image, seed
410
 
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  def generate_and_upload(
413
  model_name,
414
  prompt,
@@ -560,90 +627,127 @@ with gr.Blocks(fill_height=True) as demo:
560
  elem_classes="header-text"
561
  )
562
 
563
- with gr.Row(equal_height=False):
564
- # Left column - Input controls
565
- with gr.Column(scale=1, min_width=320):
566
- model_name = gr.Dropdown(
567
- choices=MODEL_CHOICES,
568
- value=MODEL_ZIMAGE,
569
- label="🧠 Model",
570
- info="Moody Pro Mix: fast & general (Z-Image) • One Obsession: anime / illustration (SDXL)",
571
- )
572
-
573
- prompt = gr.Textbox(
574
- label="✨ Your Prompt",
575
- placeholder="Describe the image you want to create...",
576
- lines=5,
577
- max_lines=10,
578
- autofocus=True,
579
- )
580
 
581
- with gr.Accordion("⚙️ Advanced Settings", open=False):
582
- use_negative_prompt = gr.Checkbox(
583
- label="Use Negative Prompt",
584
- value=True,
585
- visible=False, # NoobXL only
586
- )
587
- negative_prompt = gr.Textbox(
588
- label="🚫 Negative Prompt",
589
- value=NOOBXL_NEGATIVE,
590
- lines=3,
591
- max_lines=6,
592
- visible=False, # NoobXL only
593
- )
594
-
595
- with gr.Row():
596
- height = gr.Slider(
597
- minimum=512, maximum=2048, value=1024, step=64,
598
- label="Height", info="Image height in pixels",
599
  )
600
- width = gr.Slider(
601
- minimum=512, maximum=2048, value=1024, step=64,
602
- label="Width", info="Image width in pixels",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  )
604
 
605
- num_inference_steps = gr.Slider(
606
- minimum=1, maximum=50, value=9, step=1,
607
- label="Inference Steps",
608
- info="Moody Pro Mix: ~9 • One Obsession: ~28",
609
- )
610
-
611
- guidance_scale = gr.Slider(
612
- minimum=0.0, maximum=10.0, value=0.0, step=0.1,
613
- label="Guidance Scale",
614
- info="Moody Pro Mix (Z-Image) is guidance-free (forced to 0)",
615
- interactive=False,
616
- )
617
-
618
- with gr.Row():
619
- randomize_seed = gr.Checkbox(label="🎲 Random Seed", value=True)
620
- seed = gr.Number(label="Seed", value=42, precision=0, minimum=0, maximum=MAX_SEED)
621
-
622
- generate_btn = gr.Button(
623
- "🚀 Generate Image", variant="primary", size="lg", scale=1
624
- )
625
 
626
- gr.Examples(
627
- examples=examples,
628
- inputs=[prompt],
629
- label="💡 Try these prompts",
630
- examples_per_page=5,
631
- )
 
 
 
 
 
 
 
 
632
 
633
- # Right column - Output
634
- with gr.Column(scale=1, min_width=320):
635
- output_image = gr.Image(
636
- label="Generated Image",
637
- type="pil",
638
- format="png",
639
- show_label=False,
640
- height=600,
641
- buttons=["download", "share"],
642
- )
643
- used_seed = gr.Number(
644
- label="🎲 Seed Used", interactive=False, container=True,
645
  )
646
- r2_status = gr.JSON(label="☁️ R2 Upload")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
 
648
  gr.Markdown(
649
  """
@@ -682,6 +786,14 @@ with gr.Blocks(fill_height=True) as demo:
682
  outputs=[output_image, used_seed, r2_status],
683
  )
684
 
 
 
 
 
 
 
 
 
685
  if __name__ == "__main__":
686
  demo.launch(
687
  theme=custom_theme,
 
16
  )
17
  from compel import Compel, ReturnedEmbeddingsType
18
  from huggingface_hub import hf_hub_download
19
+ from transformers import AutoProcessor, AutoModelForImageTextToText
20
 
21
  import r2_uploader
22
 
 
108
 
109
  print("Pipelines loaded!")
110
 
111
+ # -----------------------------------------------------------------------------
112
+ # Qwen3.5-4B multimodal assistant. Powers the "Prompt Assistant" tab: one-turn
113
+ # question answering with an optional image, for turning rough ideas into rich
114
+ # prompts or describing a reference image. Loaded once, bf16 (~9-10 GB) — fits
115
+ # alongside the two diffusion pipelines on the shared GPU.
116
+ # -----------------------------------------------------------------------------
117
+ VLM_MODEL_ID = "rodrigomt/Qwen3.5-4B-Uncensored-Aggressive"
118
+
119
+ print("Loading Qwen3.5-4B assistant...")
120
+ vlm_processor = AutoProcessor.from_pretrained(
121
+ VLM_MODEL_ID, token=HF_TOKEN, trust_remote_code=True
122
+ )
123
+ vlm_model = AutoModelForImageTextToText.from_pretrained(
124
+ VLM_MODEL_ID,
125
+ torch_dtype=torch.bfloat16,
126
+ token=HF_TOKEN,
127
+ trust_remote_code=True,
128
+ )
129
+ vlm_model.to("cuda").eval()
130
+
131
+ print("Assistant loaded!")
132
+
133
 
134
  # =============================================================================
135
  # Compel-based prompt weighting helpers (ported from the NoobXL11 reference
 
432
  return image, seed
433
 
434
 
435
+ # =============================================================================
436
+ # Prompt Assistant (Qwen3.5-4B) — single-turn chat, optional image
437
+ # =============================================================================
438
+ @spaces.GPU
439
+ def vlm_chat(message, image, max_new_tokens, progress=gr.Progress(track_tqdm=True)):
440
+ """Answer a single user message, optionally grounded on an uploaded image."""
441
+ message = (message or "").strip()
442
+ if not message and image is None:
443
+ return "Please enter a question (and optionally attach an image)."
444
+
445
+ _gpu_start = time.time()
446
+ try:
447
+ content = []
448
+ if image is not None:
449
+ content.append({"type": "image", "image": image})
450
+ content.append({"type": "text", "text": message or "Describe this image."})
451
+ messages = [{"role": "user", "content": content}]
452
+
453
+ inputs = vlm_processor.apply_chat_template(
454
+ messages,
455
+ tokenize=True,
456
+ add_generation_prompt=True,
457
+ return_dict=True,
458
+ return_tensors="pt",
459
+ ).to(vlm_model.device)
460
+
461
+ with torch.inference_mode():
462
+ generated = vlm_model.generate(
463
+ **inputs,
464
+ max_new_tokens=int(max_new_tokens),
465
+ do_sample=False,
466
+ )
467
+ # Drop the prompt tokens so only the freshly generated answer is decoded.
468
+ trimmed = generated[0][inputs["input_ids"].shape[1]:]
469
+ text = vlm_processor.decode(trimmed, skip_special_tokens=True)
470
+ return text.strip()
471
+ finally:
472
+ print(
473
+ f"[ImageStudio] Assistant GPU time: {time.time() - _gpu_start:.2f}s "
474
+ f"(has_image={image is not None}, max_new_tokens={int(max_new_tokens)})",
475
+ flush=True,
476
+ )
477
+
478
+
479
  def generate_and_upload(
480
  model_name,
481
  prompt,
 
627
  elem_classes="header-text"
628
  )
629
 
630
+ with gr.Tabs():
631
+ with gr.Tab("🎨 Generate"):
632
+ with gr.Row(equal_height=False):
633
+ # Left column - Input controls
634
+ with gr.Column(scale=1, min_width=320):
635
+ model_name = gr.Dropdown(
636
+ choices=MODEL_CHOICES,
637
+ value=MODEL_ZIMAGE,
638
+ label="🧠 Model",
639
+ info="Moody Pro Mix: fast & general (Z-Image) • One Obsession: anime / illustration (SDXL)",
640
+ )
 
 
 
 
 
 
641
 
642
+ prompt = gr.Textbox(
643
+ label="✨ Your Prompt",
644
+ placeholder="Describe the image you want to create...",
645
+ lines=5,
646
+ max_lines=10,
647
+ autofocus=True,
 
 
 
 
 
 
 
 
 
 
 
 
648
  )
649
+
650
+ with gr.Accordion("⚙️ Advanced Settings", open=False):
651
+ use_negative_prompt = gr.Checkbox(
652
+ label="Use Negative Prompt",
653
+ value=True,
654
+ visible=False, # NoobXL only
655
+ )
656
+ negative_prompt = gr.Textbox(
657
+ label="🚫 Negative Prompt",
658
+ value=NOOBXL_NEGATIVE,
659
+ lines=3,
660
+ max_lines=6,
661
+ visible=False, # NoobXL only
662
+ )
663
+
664
+ with gr.Row():
665
+ height = gr.Slider(
666
+ minimum=512, maximum=2048, value=1024, step=64,
667
+ label="Height", info="Image height in pixels",
668
+ )
669
+ width = gr.Slider(
670
+ minimum=512, maximum=2048, value=1024, step=64,
671
+ label="Width", info="Image width in pixels",
672
+ )
673
+
674
+ num_inference_steps = gr.Slider(
675
+ minimum=1, maximum=50, value=9, step=1,
676
+ label="Inference Steps",
677
+ info="Moody Pro Mix: ~9 • One Obsession: ~28",
678
+ )
679
+
680
+ guidance_scale = gr.Slider(
681
+ minimum=0.0, maximum=10.0, value=0.0, step=0.1,
682
+ label="Guidance Scale",
683
+ info="Moody Pro Mix (Z-Image) is guidance-free (forced to 0)",
684
+ interactive=False,
685
+ )
686
+
687
+ with gr.Row():
688
+ randomize_seed = gr.Checkbox(label="🎲 Random Seed", value=True)
689
+ seed = gr.Number(label="Seed", value=42, precision=0, minimum=0, maximum=MAX_SEED)
690
+
691
+ generate_btn = gr.Button(
692
+ "🚀 Generate Image", variant="primary", size="lg", scale=1
693
  )
694
 
695
+ gr.Examples(
696
+ examples=examples,
697
+ inputs=[prompt],
698
+ label="💡 Try these prompts",
699
+ examples_per_page=5,
700
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
 
702
+ # Right column - Output
703
+ with gr.Column(scale=1, min_width=320):
704
+ output_image = gr.Image(
705
+ label="Generated Image",
706
+ type="pil",
707
+ format="png",
708
+ show_label=False,
709
+ height=600,
710
+ buttons=["download", "share"],
711
+ )
712
+ used_seed = gr.Number(
713
+ label="🎲 Seed Used", interactive=False, container=True,
714
+ )
715
+ r2_status = gr.JSON(label="☁️ R2 Upload")
716
 
717
+ with gr.Tab("💬 Prompt Assistant"):
718
+ gr.Markdown(
719
+ "Ask **Qwen3.5-4B** a single question — with or without an image. "
720
+ "Turn a rough idea into a rich prompt, or upload a reference image and "
721
+ "ask the model to describe it as a prompt."
 
 
 
 
 
 
 
722
  )
723
+ with gr.Row(equal_height=False):
724
+ with gr.Column(scale=1, min_width=320):
725
+ vlm_image = gr.Image(
726
+ label="🖼️ Image (optional)",
727
+ type="pil",
728
+ height=320,
729
+ )
730
+ vlm_prompt = gr.Textbox(
731
+ label="💬 Your Question",
732
+ placeholder="e.g. 'Improve this prompt: a cat in a hat' — or attach an image and ask 'describe this as a prompt'",
733
+ lines=4,
734
+ max_lines=12,
735
+ )
736
+ with gr.Accordion("⚙️ Settings", open=False):
737
+ vlm_max_tokens = gr.Slider(
738
+ minimum=64, maximum=2048, value=512, step=64,
739
+ label="Max New Tokens",
740
+ info="Upper bound on the answer length",
741
+ )
742
+ vlm_btn = gr.Button(
743
+ "✨ Ask", variant="primary", size="lg", scale=1
744
+ )
745
+ with gr.Column(scale=1, min_width=320):
746
+ vlm_output = gr.Textbox(
747
+ label="🤖 Answer",
748
+ lines=20,
749
+ show_copy_button=True,
750
+ )
751
 
752
  gr.Markdown(
753
  """
 
786
  outputs=[output_image, used_seed, r2_status],
787
  )
788
 
789
+ # Prompt Assistant (Qwen3.5-4B) — single-turn, optional image
790
+ vlm_inputs = [vlm_prompt, vlm_image, vlm_max_tokens]
791
+ vlm_btn.click(
792
+ fn=vlm_chat, inputs=vlm_inputs, outputs=[vlm_output],
793
+ api_name="prompt_assistant",
794
+ )
795
+ vlm_prompt.submit(fn=vlm_chat, inputs=vlm_inputs, outputs=[vlm_output])
796
+
797
  if __name__ == "__main__":
798
  demo.launch(
799
  theme=custom_theme,