Spaces:
Running on Zero
Running on Zero
Handle unsupported/corrupt image uploads with a clear error; add HEIC support
Browse files- app.py +65 -6
- 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",
|
|
|
|
| 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
|