nsfwalex Claude Opus 4.8 commited on
Commit
288cccb
·
1 Parent(s): 3cc90b8

Switch assistant to gemma-4-12B (8-bit) + expose system prompt

Browse files

- VLM_MODEL_ID -> OpenYourMind/gemma-4-12B-it-abliterated-uncensored
(model_type gemma4_unified). Load 8-bit via bitsandbytes by default
(~12GB, fits the zero-a10g alongside the diffusion pipelines);
VLM_LOAD_8BIT=0 reverts to bf16. Prior models kept commented.
- requirements: transformers -> git main (gemma4_unified support) + bitsandbytes.
- Expose an optional system_prompt through _vlm_chat_core (real system role
when the template supports it, folded into the user turn otherwise) and
surface it in the Prompt Assistant UI + the prompt_assistant /
prompt_to_video_assets / image_to_video_assets API endpoints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +95 -44
  2. requirements.txt +3 -1
app.py CHANGED
@@ -20,7 +20,12 @@ from diffusers import (
20
  )
21
  from compel import Compel, ReturnedEmbeddingsType
22
  from huggingface_hub import hf_hub_download
23
- from transformers import AutoProcessor, AutoModelForImageTextToText, TextIteratorStreamer
 
 
 
 
 
24
 
25
  # Structured progress contract (also see wan2.2 / LTX2.3 Spaces and the generator
26
  # orchestrator). Every streaming endpoint yields a hidden JSON "progress" output
@@ -145,26 +150,36 @@ print("Pipelines loaded!")
145
  # describing a reference image. Loaded once, bf16 — fits alongside the two
146
  # diffusion pipelines on the shared GPU.
147
  # -----------------------------------------------------------------------------
148
- # Prior models, kept for easy revert:
149
  # "rodrigomt/Qwen3.5-4B-Uncensored-Aggressive" # no generation_config; needed eos pinning
150
- # "prithivMLmods/gemma-4-E4B-it-Uncensored-MAX" # under-rated explicit content (rating capped ~2)
151
- # Current: 9B uncensored Qwen3.5 VL. Vision-capable (image-text-to-text). NOTE its
152
- # generation_config eos is just <|endoftext|> and omits the chat terminator
153
- # <|im_end|>; _resolve_vlm_eos_ids() unions both so stopping still works. ~18.8 GB
154
- # bf16 — heaviest assistant so far; watch GPU memory alongside the diffusers.
155
- VLM_MODEL_ID = "ccharnkij/Qwen3.5-9B-Uncensored"
156
-
157
- print(f"Loading assistant: {VLM_MODEL_ID} ...")
 
 
 
 
 
 
158
  vlm_processor = AutoProcessor.from_pretrained(
159
  VLM_MODEL_ID, token=HF_TOKEN, trust_remote_code=True
160
  )
161
- vlm_model = AutoModelForImageTextToText.from_pretrained(
162
- VLM_MODEL_ID,
163
- torch_dtype=torch.bfloat16,
164
- token=HF_TOKEN,
165
- trust_remote_code=True,
166
- )
167
- vlm_model.to("cuda").eval()
 
 
 
 
168
 
169
 
170
  def _resolve_vlm_eos_ids(tokenizer, model):
@@ -671,15 +686,21 @@ def _generate_image_inner(
671
  # =============================================================================
672
  # Prompt Assistant (Qwen3.5-4B) — single-turn chat, optional image
673
  # =============================================================================
674
- def _vlm_chat_core(message, image, reasoning, max_new_tokens):
675
  """Token-streaming VLM chat body (no GPU decorator).
676
 
677
  Yields ``("progress", produced, budget, partial)`` per token and a final
678
  ``("text", answer)`` tuple. Kept decorator-free so it can be reused inside a
679
  *single* ``@spaces.GPU`` window by other endpoints (e.g. the image→video
680
  check), avoiding a second ZeroGPU acquisition. See :func:`vlm_chat`.
 
 
 
 
 
681
  """
682
  message = (message or "").strip()
 
683
  if not message and image is None:
684
  yield ("text", "Please enter a question (and optionally attach an image).")
685
  return
@@ -688,20 +709,38 @@ def _vlm_chat_core(message, image, reasoning, max_new_tokens):
688
  budget = int(max_new_tokens)
689
  _gpu_start = time.time()
690
 
 
691
  content = []
692
  if image is not None:
693
  content.append({"type": "image", "image": image})
694
- content.append({"type": "text", "text": message or "Describe this image."})
695
- messages = [{"role": "user", "content": content}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
 
697
- inputs = vlm_processor.apply_chat_template(
698
- messages,
699
- tokenize=True,
700
- add_generation_prompt=True,
701
- return_dict=True,
702
- return_tensors="pt",
703
- enable_thinking=enable_thinking,
704
- ).to(vlm_model.device)
 
 
 
705
 
706
  tokenizer = getattr(vlm_processor, "tokenizer", vlm_processor)
707
  streamer = TextIteratorStreamer(
@@ -759,19 +798,20 @@ def _vlm_chat_core(message, image, reasoning, max_new_tokens):
759
 
760
 
761
  @spaces.GPU
762
- def vlm_chat(message, image, reasoning, max_new_tokens, progress=gr.Progress(track_tqdm=True)):
763
  """Answer a single user message, optionally grounded on an uploaded image.
764
 
765
  Thin ``@spaces.GPU`` wrapper around :func:`_vlm_chat_core` (which holds the
766
- token-streaming logic). ``reasoning`` ("On"/"Off") drives Qwen's
767
  ``enable_thinking`` switch: Off skips the <think> trace for a direct answer
768
  (best for prompt rewriting); On lets the model reason first (slower).
 
769
  """
770
- yield from _vlm_chat_core(message, image, reasoning, max_new_tokens)
771
 
772
 
773
  @spaces.GPU
774
- def _image_video_check_gpu(image, message, reasoning, max_new_tokens):
775
  """One GPU window: moderate ``image``, then stream a video prompt from it.
776
 
777
  Yields ``("moderation", dict)`` once the image-context check is done, then
@@ -785,7 +825,7 @@ def _image_video_check_gpu(image, message, reasoning, max_new_tokens):
785
  moderation = {"ok": False, "rating": None, "confidence": None,
786
  "flags": None, "raw": "", "error": f"{type(exc).__name__}: {exc}"}
787
  yield ("moderation", moderation)
788
- yield from _vlm_chat_core(message, image, reasoning, max_new_tokens)
789
 
790
 
791
  def generate_and_upload(
@@ -869,16 +909,17 @@ def generate_and_upload(
869
 
870
 
871
  def assistant_chat(
872
- message, image, reasoning, max_new_tokens,
873
  progress=gr.Progress(track_tqdm=True),
874
  ):
875
  """Gradio-facing wrapper around ``vlm_chat``.
876
 
877
  Yields ``(answer, progress)`` so the UI streams the text live and a downstream
878
  consumer reading the progress index sees this node advance over SSE. The final
879
- ``complete`` frame carries the clean answer at index 0.
 
880
  """
881
- for ev in vlm_chat(message, image, reasoning, max_new_tokens, progress=progress):
882
  if ev[0] == "progress":
883
  _, produced, budget, partial = ev
884
  frac = min(0.99, produced / max(budget, 1))
@@ -934,6 +975,7 @@ def prompt_to_video_assets(
934
  randomize_seed: bool = True,
935
  reasoning: str = "Off",
936
  max_new_tokens: int = 512,
 
937
  request: gr.Request = None,
938
  ) -> dict:
939
  """Text -> first-frame image (uploaded to R2) + video generation prompt.
@@ -977,7 +1019,7 @@ def prompt_to_video_assets(
977
  frame_prompt = ""
978
  for ev in vlm_chat(
979
  _compose_instruction(image_instruction, original_input),
980
- None, reasoning, max_new_tokens,
981
  ):
982
  if ev[0] == "progress":
983
  _, produced, budget, partial = ev
@@ -1044,7 +1086,7 @@ def prompt_to_video_assets(
1044
  video_prompt = ""
1045
  for ev in vlm_chat(
1046
  _compose_instruction(video_instruction, original_input),
1047
- image, reasoning, max_new_tokens,
1048
  ):
1049
  if ev[0] == "progress":
1050
  _, produced, budget, partial = ev
@@ -1088,6 +1130,7 @@ def image_to_video_assets(
1088
  video_instruction,
1089
  reasoning,
1090
  max_new_tokens,
 
1091
  progress=gr.Progress(track_tqdm=True),
1092
  ):
1093
  """User-uploaded image -> motion prompt + image-context (moderation) check.
@@ -1110,7 +1153,7 @@ def image_to_video_assets(
1110
  video_prompt = ""
1111
  yield video_prompt, status, _progress("check", 0.02, label="Screening image")
1112
 
1113
- for ev in _image_video_check_gpu(image, instruction, reasoning, budget):
1114
  if ev[0] == "moderation":
1115
  status = {"moderation": ev[1]}
1116
  yield video_prompt, status, _progress("check", 0.2, label="Image checked")
@@ -1333,9 +1376,10 @@ with gr.Blocks(fill_height=True) as demo:
1333
 
1334
  with gr.Tab("💬 Prompt Assistant"):
1335
  gr.Markdown(
1336
- "Ask **Qwen3.5-9B** a single question — with or without an image. "
1337
  "Turn a rough idea into a rich prompt, or upload a reference image and "
1338
- "ask the model to describe it as a prompt."
 
1339
  )
1340
  with gr.Row(equal_height=False):
1341
  with gr.Column(scale=1, min_width=320):
@@ -1344,6 +1388,12 @@ with gr.Blocks(fill_height=True) as demo:
1344
  type="pil",
1345
  height=320,
1346
  )
 
 
 
 
 
 
1347
  vlm_prompt = gr.Textbox(
1348
  label="💬 Your Question",
1349
  placeholder="e.g. 'Improve this prompt: a cat in a hat' — or attach an image and ask 'describe this as a prompt'",
@@ -1413,8 +1463,8 @@ with gr.Blocks(fill_height=True) as demo:
1413
  outputs=[output_image, used_seed, r2_status, gen_progress],
1414
  )
1415
 
1416
- # Prompt Assistant (Qwen3.5-4B) — single-turn, optional image
1417
- vlm_inputs = [vlm_prompt, vlm_image, vlm_reasoning, vlm_max_tokens]
1418
  vlm_btn.click(
1419
  fn=assistant_chat, inputs=vlm_inputs, outputs=[vlm_output, vlm_progress],
1420
  api_name="prompt_assistant",
@@ -1432,13 +1482,14 @@ with gr.Blocks(fill_height=True) as demo:
1432
  i2v_instruction = gr.Textbox(label="i2v instruction", value="")
1433
  i2v_reasoning = gr.Radio(choices=["Off", "On"], value="Off", label="i2v reasoning")
1434
  i2v_max_tokens = gr.Slider(minimum=64, maximum=1024, value=256, step=64, label="i2v max tokens")
 
1435
  i2v_btn = gr.Button("i2v", visible=False)
1436
  i2v_prompt_out = gr.Textbox(label="i2v prompt")
1437
  i2v_status_out = gr.JSON(label="i2v status")
1438
  i2v_progress_out = gr.JSON(label="i2v progress")
1439
  i2v_btn.click(
1440
  fn=image_to_video_assets,
1441
- inputs=[i2v_image, i2v_instruction, i2v_reasoning, i2v_max_tokens],
1442
  outputs=[i2v_prompt_out, i2v_status_out, i2v_progress_out],
1443
  api_name="image_to_video_assets",
1444
  )
 
20
  )
21
  from compel import Compel, ReturnedEmbeddingsType
22
  from huggingface_hub import hf_hub_download
23
+ from transformers import (
24
+ AutoProcessor,
25
+ AutoModelForImageTextToText,
26
+ BitsAndBytesConfig,
27
+ TextIteratorStreamer,
28
+ )
29
 
30
  # Structured progress contract (also see wan2.2 / LTX2.3 Spaces and the generator
31
  # orchestrator). Every streaming endpoint yields a hidden JSON "progress" output
 
150
  # describing a reference image. Loaded once, bf16 — fits alongside the two
151
  # diffusion pipelines on the shared GPU.
152
  # -----------------------------------------------------------------------------
153
+ # Prior models, kept for easy revert (set VLM_MODEL_ID back to one of these):
154
  # "rodrigomt/Qwen3.5-4B-Uncensored-Aggressive" # no generation_config; needed eos pinning
155
+ # "prithivMLmods/gemma-4-E4B-it-Uncensored-MAX" # model_type gemma4; under-rated explicit content (~2)
156
+ # "ccharnkij/Qwen3.5-9B-Uncensored" # 9B Qwen3.5 VL, ~18.8 GB bf16 (the previous default)
157
+ # Current: gemma-4-12B "unified" (text+image+audio+video), abliterated/uncensored.
158
+ # model_type=gemma4_unified / Gemma4UnifiedForConditionalGeneration — NEWER than the
159
+ # earlier gemma4, so it needs a very recent transformers (requirements pins git main).
160
+ # It's ~24 GB in bf16, which does NOT fit on the zero-a10g (~24 GB) next to the two
161
+ # diffusion pipelines, so we load it 8-bit (bitsandbytes, ~12 GB) by DEFAULT.
162
+ # Flip VLM_LOAD_8BIT=0 to load full bf16 (only safe on a bigger GPU). Stop tokens are
163
+ # handled model-agnostically by _resolve_vlm_eos_ids() (this model ships a proper
164
+ # generation_config + chat_template.jinja).
165
+ VLM_MODEL_ID = os.environ.get("VLM_MODEL_ID", "OpenYourMind/gemma-4-12B-it-abliterated-uncensored")
166
+ VLM_LOAD_8BIT = os.environ.get("VLM_LOAD_8BIT", "1").lower() not in ("0", "false", "no", "")
167
+
168
+ print(f"Loading assistant: {VLM_MODEL_ID} (8bit={VLM_LOAD_8BIT}) ...")
169
  vlm_processor = AutoProcessor.from_pretrained(
170
  VLM_MODEL_ID, token=HF_TOKEN, trust_remote_code=True
171
  )
172
+ _vlm_kwargs = dict(token=HF_TOKEN, trust_remote_code=True, torch_dtype=torch.bfloat16)
173
+ if VLM_LOAD_8BIT:
174
+ # bitsandbytes quantizes the weights on GPU as they load; device_map places them
175
+ # there directly, so we must NOT call .to("cuda") afterwards (it errors on a
176
+ # quantized model).
177
+ _vlm_kwargs["quantization_config"] = BitsAndBytesConfig(load_in_8bit=True)
178
+ _vlm_kwargs["device_map"] = "auto"
179
+ vlm_model = AutoModelForImageTextToText.from_pretrained(VLM_MODEL_ID, **_vlm_kwargs)
180
+ if not VLM_LOAD_8BIT:
181
+ vlm_model.to("cuda")
182
+ vlm_model.eval()
183
 
184
 
185
  def _resolve_vlm_eos_ids(tokenizer, model):
 
686
  # =============================================================================
687
  # Prompt Assistant (Qwen3.5-4B) — single-turn chat, optional image
688
  # =============================================================================
689
+ def _vlm_chat_core(message, image, reasoning, max_new_tokens, system_prompt=""):
690
  """Token-streaming VLM chat body (no GPU decorator).
691
 
692
  Yields ``("progress", produced, budget, partial)`` per token and a final
693
  ``("text", answer)`` tuple. Kept decorator-free so it can be reused inside a
694
  *single* ``@spaces.GPU`` window by other endpoints (e.g. the image→video
695
  check), avoiding a second ZeroGPU acquisition. See :func:`vlm_chat`.
696
+
697
+ ``system_prompt`` (optional) steers the assistant. Models whose chat template
698
+ supports a ``system`` role (e.g. Qwen) get a real system turn; templates that
699
+ reject one (e.g. Gemma) fall back to folding the system text into the user
700
+ turn, so the param works regardless of which model is loaded.
701
  """
702
  message = (message or "").strip()
703
+ system_prompt = (system_prompt or "").strip()
704
  if not message and image is None:
705
  yield ("text", "Please enter a question (and optionally attach an image).")
706
  return
 
709
  budget = int(max_new_tokens)
710
  _gpu_start = time.time()
711
 
712
+ user_text = message or "Describe this image."
713
  content = []
714
  if image is not None:
715
  content.append({"type": "image", "image": image})
716
+ content.append({"type": "text", "text": user_text})
717
+
718
+ messages = []
719
+ if system_prompt:
720
+ messages.append({"role": "system", "content": [{"type": "text", "text": system_prompt}]})
721
+ messages.append({"role": "user", "content": content})
722
+
723
+ def _template(msgs):
724
+ return vlm_processor.apply_chat_template(
725
+ msgs,
726
+ tokenize=True,
727
+ add_generation_prompt=True,
728
+ return_dict=True,
729
+ return_tensors="pt",
730
+ enable_thinking=enable_thinking,
731
+ )
732
 
733
+ try:
734
+ inputs = _template(messages).to(vlm_model.device)
735
+ except Exception:
736
+ # Template has no system role (Gemma et al.) — fold it into the user turn.
737
+ if not system_prompt:
738
+ raise
739
+ folded = []
740
+ if image is not None:
741
+ folded.append({"type": "image", "image": image})
742
+ folded.append({"type": "text", "text": f"{system_prompt}\n\n{user_text}"})
743
+ inputs = _template([{"role": "user", "content": folded}]).to(vlm_model.device)
744
 
745
  tokenizer = getattr(vlm_processor, "tokenizer", vlm_processor)
746
  streamer = TextIteratorStreamer(
 
798
 
799
 
800
  @spaces.GPU
801
+ def vlm_chat(message, image, reasoning, max_new_tokens, system_prompt="", progress=gr.Progress(track_tqdm=True)):
802
  """Answer a single user message, optionally grounded on an uploaded image.
803
 
804
  Thin ``@spaces.GPU`` wrapper around :func:`_vlm_chat_core` (which holds the
805
+ token-streaming logic). ``reasoning`` ("On"/"Off") drives the model's
806
  ``enable_thinking`` switch: Off skips the <think> trace for a direct answer
807
  (best for prompt rewriting); On lets the model reason first (slower).
808
+ ``system_prompt`` (optional) steers the assistant.
809
  """
810
+ yield from _vlm_chat_core(message, image, reasoning, max_new_tokens, system_prompt)
811
 
812
 
813
  @spaces.GPU
814
+ def _image_video_check_gpu(image, message, reasoning, max_new_tokens, system_prompt=""):
815
  """One GPU window: moderate ``image``, then stream a video prompt from it.
816
 
817
  Yields ``("moderation", dict)`` once the image-context check is done, then
 
825
  moderation = {"ok": False, "rating": None, "confidence": None,
826
  "flags": None, "raw": "", "error": f"{type(exc).__name__}: {exc}"}
827
  yield ("moderation", moderation)
828
+ yield from _vlm_chat_core(message, image, reasoning, max_new_tokens, system_prompt)
829
 
830
 
831
  def generate_and_upload(
 
909
 
910
 
911
  def assistant_chat(
912
+ message, image, reasoning, max_new_tokens, system_prompt="",
913
  progress=gr.Progress(track_tqdm=True),
914
  ):
915
  """Gradio-facing wrapper around ``vlm_chat``.
916
 
917
  Yields ``(answer, progress)`` so the UI streams the text live and a downstream
918
  consumer reading the progress index sees this node advance over SSE. The final
919
+ ``complete`` frame carries the clean answer at index 0. ``system_prompt``
920
+ (optional) steers the assistant.
921
  """
922
+ for ev in vlm_chat(message, image, reasoning, max_new_tokens, system_prompt, progress=progress):
923
  if ev[0] == "progress":
924
  _, produced, budget, partial = ev
925
  frac = min(0.99, produced / max(budget, 1))
 
975
  randomize_seed: bool = True,
976
  reasoning: str = "Off",
977
  max_new_tokens: int = 512,
978
+ system_prompt: str = "",
979
  request: gr.Request = None,
980
  ) -> dict:
981
  """Text -> first-frame image (uploaded to R2) + video generation prompt.
 
1019
  frame_prompt = ""
1020
  for ev in vlm_chat(
1021
  _compose_instruction(image_instruction, original_input),
1022
+ None, reasoning, max_new_tokens, system_prompt,
1023
  ):
1024
  if ev[0] == "progress":
1025
  _, produced, budget, partial = ev
 
1086
  video_prompt = ""
1087
  for ev in vlm_chat(
1088
  _compose_instruction(video_instruction, original_input),
1089
+ image, reasoning, max_new_tokens, system_prompt,
1090
  ):
1091
  if ev[0] == "progress":
1092
  _, produced, budget, partial = ev
 
1130
  video_instruction,
1131
  reasoning,
1132
  max_new_tokens,
1133
+ system_prompt="",
1134
  progress=gr.Progress(track_tqdm=True),
1135
  ):
1136
  """User-uploaded image -> motion prompt + image-context (moderation) check.
 
1153
  video_prompt = ""
1154
  yield video_prompt, status, _progress("check", 0.02, label="Screening image")
1155
 
1156
+ for ev in _image_video_check_gpu(image, instruction, reasoning, budget, system_prompt):
1157
  if ev[0] == "moderation":
1158
  status = {"moderation": ev[1]}
1159
  yield video_prompt, status, _progress("check", 0.2, label="Image checked")
 
1376
 
1377
  with gr.Tab("💬 Prompt Assistant"):
1378
  gr.Markdown(
1379
+ "Ask the assistant model a single question — with or without an image. "
1380
  "Turn a rough idea into a rich prompt, or upload a reference image and "
1381
+ "ask the model to describe it as a prompt. Use the **System Prompt** to "
1382
+ "steer its tone/role."
1383
  )
1384
  with gr.Row(equal_height=False):
1385
  with gr.Column(scale=1, min_width=320):
 
1388
  type="pil",
1389
  height=320,
1390
  )
1391
+ vlm_system = gr.Textbox(
1392
+ label="🧭 System Prompt (optional)",
1393
+ placeholder="e.g. 'You are an expert Danbooru tag writer. Output only comma-separated tags.'",
1394
+ lines=2,
1395
+ max_lines=8,
1396
+ )
1397
  vlm_prompt = gr.Textbox(
1398
  label="💬 Your Question",
1399
  placeholder="e.g. 'Improve this prompt: a cat in a hat' — or attach an image and ask 'describe this as a prompt'",
 
1463
  outputs=[output_image, used_seed, r2_status, gen_progress],
1464
  )
1465
 
1466
+ # Prompt Assistant — single-turn, optional image, optional system prompt
1467
+ vlm_inputs = [vlm_prompt, vlm_image, vlm_reasoning, vlm_max_tokens, vlm_system]
1468
  vlm_btn.click(
1469
  fn=assistant_chat, inputs=vlm_inputs, outputs=[vlm_output, vlm_progress],
1470
  api_name="prompt_assistant",
 
1482
  i2v_instruction = gr.Textbox(label="i2v instruction", value="")
1483
  i2v_reasoning = gr.Radio(choices=["Off", "On"], value="Off", label="i2v reasoning")
1484
  i2v_max_tokens = gr.Slider(minimum=64, maximum=1024, value=256, step=64, label="i2v max tokens")
1485
+ i2v_system = gr.Textbox(label="i2v system prompt", value="")
1486
  i2v_btn = gr.Button("i2v", visible=False)
1487
  i2v_prompt_out = gr.Textbox(label="i2v prompt")
1488
  i2v_status_out = gr.JSON(label="i2v status")
1489
  i2v_progress_out = gr.JSON(label="i2v progress")
1490
  i2v_btn.click(
1491
  fn=image_to_video_assets,
1492
+ inputs=[i2v_image, i2v_instruction, i2v_reasoning, i2v_max_tokens, i2v_system],
1493
  outputs=[i2v_prompt_out, i2v_status_out, i2v_progress_out],
1494
  api_name="image_to_video_assets",
1495
  )
requirements.txt CHANGED
@@ -1,8 +1,10 @@
1
  gradio[mcp]
2
  git+https://github.com/huggingface/diffusers
3
- transformers
 
4
  torchvision
5
  accelerate
 
6
  compel
7
  sentencepiece
8
  numpy
 
1
  gradio[mcp]
2
  git+https://github.com/huggingface/diffusers
3
+ # gemma-4-12B is model_type gemma4_unified — needs a very recent transformers.
4
+ git+https://github.com/huggingface/transformers
5
  torchvision
6
  accelerate
7
+ bitsandbytes
8
  compel
9
  sentencepiece
10
  numpy