JonathanColetti commited on
Commit
5d3a980
·
verified ·
1 Parent(s): b1d84a1

Handle unsupported/corrupt image uploads with a clear error; add HEIC support

Browse files
Files changed (2) hide show
  1. app.py +65 -6
  2. requirements.txt +3 -0
app.py CHANGED
@@ -1,8 +1,10 @@
1
  import os
 
2
  import spaces
3
 
4
  import torch
5
  import gradio as gr
 
6
  from threading import Thread
7
  from transformers import (AutoProcessor, AutoModelForImageTextToText,
8
  BitsAndBytesConfig, TextIteratorStreamer)
@@ -33,6 +35,55 @@ model = AutoModelForImageTextToText.from_pretrained(MODEL_ID, **load_kwargs)
33
  model.eval()
34
 
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  def to_messages(message, history):
37
  messages = []
38
  for turn in history:
@@ -42,13 +93,14 @@ def to_messages(message, history):
42
  continue
43
  if isinstance(content, (tuple, list)):
44
  messages.append({"role": role,
45
- "content": [{"type": "image", "image": content[0]}]})
 
46
  else:
47
  messages.append({"role": role, "content": [{"type": "text", "text": content}]})
48
 
49
  content = []
50
  for path in message.get("files") or []:
51
- content.append({"type": "image", "image": path})
52
  if message.get("text"):
53
  content.append({"type": "text", "text": message["text"]})
54
  messages.append({"role": "user", "content": content})
@@ -70,10 +122,6 @@ def split_thinking(text):
70
  return [gr.ChatMessage(role="assistant", content=text)]
71
 
72
 
73
- # size='xlarge': this is a 27B at bf16 (~54.7 GB packed), which does not fit the default
74
- # ZeroGPU slice -- the symptom was "GPU task aborted" with nothing in the Space logs.
75
- # duration is kept at 120s, the usual ZeroGPU ceiling; 150 was over it.
76
- @spaces.GPU(duration=120, size="xlarge")
77
  def respond(message: dict, history: list, temperature: float, top_p: float, top_k: int):
78
  """Chat with Qwen3.8-27B-Uncensored. Accepts text and images, streams the reply.
79
 
@@ -84,7 +132,18 @@ def respond(message: dict, history: list, temperature: float, top_p: float, top_
84
  top_p: Nucleus sampling cutoff.
85
  top_k: Top-k sampling cutoff.
86
  """
 
 
 
87
  messages = to_messages(message, history)
 
 
 
 
 
 
 
 
88
  inputs = processor.apply_chat_template(
89
  messages,
90
  add_generation_prompt=True,
 
1
  import os
2
+ import tempfile
3
  import spaces
4
 
5
  import torch
6
  import gradio as gr
7
+ from PIL import Image
8
  from threading import Thread
9
  from transformers import (AutoProcessor, AutoModelForImageTextToText,
10
  BitsAndBytesConfig, TextIteratorStreamer)
 
35
  model.eval()
36
 
37
 
38
+ # transformers' image loader delegates to torchvision.decode_image, which accepts only
39
+ # jpeg/png/webp/gif and raises a bare RuntimeError on anything else -- the user saw a
40
+ # 30-line traceback for an ordinary phone photo. Normalising through PIL first means
41
+ # BMP/TIFF/ICO (and HEIC, when pillow-heif is installed) work instead of failing, and
42
+ # anything genuinely unreadable produces a message that says what to do about it.
43
+ try:
44
+ import pillow_heif
45
+ pillow_heif.register_heif_opener()
46
+ _HEIF = True
47
+ except Exception:
48
+ _HEIF = False
49
+
50
+ NATIVE_FORMATS = {"JPEG", "PNG", "WEBP", "GIF"}
51
+ SUPPORTED_HINT = ("JPEG, PNG, WebP, GIF, BMP, TIFF"
52
+ + (", HEIC/HEIF" if _HEIF else ""))
53
+ _image_cache = {}
54
+
55
+
56
+ def prepare_image(path):
57
+ """Return a path the processor can definitely decode, or raise a clear gr.Error."""
58
+ cached = _image_cache.get(path)
59
+ if cached and os.path.exists(cached):
60
+ return cached
61
+
62
+ name = os.path.basename(path or "file")
63
+ try:
64
+ with Image.open(path) as probe:
65
+ probe.verify() # catches truncated/corrupt files
66
+ with Image.open(path) as img:
67
+ fmt = (img.format or "").upper()
68
+ if fmt in NATIVE_FORMATS and img.mode in ("RGB", "L"):
69
+ _image_cache[path] = path
70
+ return path
71
+ converted = img.convert("RGB")
72
+ handle, out = tempfile.mkstemp(suffix=".png")
73
+ os.close(handle)
74
+ converted.save(out, format="PNG")
75
+ except gr.Error:
76
+ raise
77
+ except Exception as exc:
78
+ raise gr.Error(
79
+ f"Couldn't read the image '{name}'. Supported formats: {SUPPORTED_HINT}. "
80
+ f"If it's an AVIF or HEIC photo, re-save it as JPEG or PNG and try again."
81
+ ) from exc
82
+
83
+ _image_cache[path] = out
84
+ return out
85
+
86
+
87
  def to_messages(message, history):
88
  messages = []
89
  for turn in history:
 
93
  continue
94
  if isinstance(content, (tuple, list)):
95
  messages.append({"role": role,
96
+ "content": [{"type": "image",
97
+ "image": prepare_image(content[0])}]})
98
  else:
99
  messages.append({"role": role, "content": [{"type": "text", "text": content}]})
100
 
101
  content = []
102
  for path in message.get("files") or []:
103
+ content.append({"type": "image", "image": prepare_image(path)})
104
  if message.get("text"):
105
  content.append({"type": "text", "text": message["text"]})
106
  messages.append({"role": "user", "content": content})
 
122
  return [gr.ChatMessage(role="assistant", content=text)]
123
 
124
 
 
 
 
 
125
  def respond(message: dict, history: list, temperature: float, top_p: float, top_k: int):
126
  """Chat with Qwen3.8-27B-Uncensored. Accepts text and images, streams the reply.
127
 
 
132
  top_p: Nucleus sampling cutoff.
133
  top_k: Top-k sampling cutoff.
134
  """
135
+ # Build (and validate) the turn outside the GPU worker, so an unreadable upload
136
+ # fails immediately with a readable message instead of consuming a ZeroGPU slot
137
+ # and surfacing as a traceback from inside the worker.
138
  messages = to_messages(message, history)
139
+ yield from generate(messages, temperature, top_p, top_k)
140
+
141
+
142
+ # size='xlarge': this is a 27B at bf16 (~54.7 GB packed), which does not fit the default
143
+ # ZeroGPU slice -- the symptom was "GPU task aborted" with nothing in the Space logs.
144
+ # duration is kept at 120s, the usual ZeroGPU ceiling; 150 was over it.
145
+ @spaces.GPU(duration=120, size="xlarge")
146
+ def generate(messages: list, temperature: float, top_p: float, top_k: int):
147
  inputs = processor.apply_chat_template(
148
  messages,
149
  add_generation_prompt=True,
requirements.txt CHANGED
@@ -5,3 +5,6 @@ transformers==5.15.0
5
  accelerate==1.14.0
6
  bitsandbytes==0.50.1
7
  torchvision
 
 
 
 
5
  accelerate==1.14.0
6
  bitsandbytes==0.50.1
7
  torchvision
8
+ # HEIC/HEIF support: torchvision's decoder rejects them outright, so phone photos
9
+ # fail without this. Optional at runtime -- app.py degrades to a clear error.
10
+ pillow-heif