Spaces:
Runtime error
Runtime error
| """SnapQuest MiniCPM-V 4.6 Modal deployment — Modal 1.4.3, Transformers 5.x | |
| Deploy: modal deploy modal_app.py | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import io | |
| from typing import Any | |
| import modal | |
| MODEL_ID = "openbmb/MiniCPM-V-4.6" | |
| image = ( | |
| modal.Image.debian_slim(python_version="3.11") | |
| .pip_install( | |
| "transformers>=5.7.0", | |
| "torch", | |
| "torchvision", | |
| "av", | |
| "Pillow", | |
| "accelerate", | |
| "huggingface_hub", | |
| "fastapi[standard]", | |
| ) | |
| .run_commands( | |
| "python -c \"" | |
| "from huggingface_hub import snapshot_download; " | |
| f"snapshot_download(repo_id='{MODEL_ID}', local_dir='/model', " | |
| "ignore_patterns=['*.gguf', 'gguf*'])" | |
| "\"" | |
| ) | |
| ) | |
| app = modal.App("snapquest-minicpm-v-46") | |
| def _extract_input(messages: list[dict[str, Any]]) -> tuple[str, str, bytes]: | |
| system_prompt = "" | |
| user_text_parts: list[str] = [] | |
| image_data = b"" | |
| for message in messages: | |
| role = message.get("role") | |
| content = message.get("content") | |
| if role == "system": | |
| if isinstance(content, str): | |
| system_prompt = content | |
| continue | |
| if role != "user": | |
| continue | |
| if isinstance(content, str): | |
| user_text_parts.append(content) | |
| continue | |
| if not isinstance(content, list): | |
| continue | |
| for block in content: | |
| if not isinstance(block, dict): | |
| continue | |
| if block.get("type") == "text": | |
| user_text_parts.append(str(block.get("text", ""))) | |
| elif block.get("type") == "image": | |
| source = block.get("source", {}) | |
| if source.get("type") == "base64": | |
| image_data = base64.b64decode(source.get("data", "")) | |
| if not image_data: | |
| raise ValueError("No base64 image found in messages.") | |
| return system_prompt, "\n".join(p for p in user_text_parts if p), image_data | |
| class MiniCPMVService: | |
| def load_model(self) -> None: | |
| import torch | |
| from transformers import AutoModelForImageTextToText, AutoProcessor | |
| self.processor = AutoProcessor.from_pretrained( | |
| "/model", trust_remote_code=True | |
| ) | |
| self.model = AutoModelForImageTextToText.from_pretrained( | |
| "/model", | |
| trust_remote_code=True, | |
| torch_dtype="auto", | |
| device_map="auto", | |
| ) | |
| self.model.eval() | |
| def analyze(self, payload: dict[str, Any]) -> dict[str, Any]: | |
| from PIL import Image | |
| import torch | |
| messages = payload.get("messages") | |
| if not isinstance(messages, list): | |
| return {"error": "messages must be a list"} | |
| temperature = float(payload.get("temperature", 0.4)) | |
| max_tokens = int(payload.get("max_tokens", 500)) | |
| try: | |
| system_prompt, user_text, image_bytes = _extract_input(messages) | |
| except ValueError as e: | |
| return {"error": str(e)} | |
| pil_image = Image.open(io.BytesIO(image_bytes)).convert("RGB") | |
| prompt = f"{system_prompt}\n\n{user_text}" if system_prompt else user_text | |
| # Correct transformers 5.x API for MiniCPM-V 4.6 | |
| msgs = [{"role": "user", "content": [{"type": "image", "image": pil_image}, {"type": "text", "text": prompt}]}] | |
| inputs = self.processor.apply_chat_template( | |
| msgs, | |
| add_generation_prompt=True, | |
| tokenize=True, | |
| return_tensors="pt", | |
| return_dict=True, | |
| ).to("cuda") | |
| with torch.inference_mode(): | |
| output_ids = self.model.generate( | |
| **inputs, | |
| max_new_tokens=max_tokens, | |
| do_sample=temperature > 0, | |
| temperature=temperature if temperature > 0 else 1.0, | |
| ) | |
| input_len = inputs["input_ids"].shape[1] | |
| answer = self.processor.decode( | |
| output_ids[0][input_len:], skip_special_tokens=True | |
| ) | |
| return {"choices": [{"message": {"content": answer}}]} | |
| if __name__ == "__main__": | |
| buffer = io.BytesIO() | |
| from PIL import Image | |
| Image.new("RGB", (64, 64), color=(32, 64, 96)).save(buffer, format="PNG") | |
| encoded = base64.b64encode(buffer.getvalue()).decode("utf-8") | |
| payload = { | |
| "messages": [ | |
| {"role": "system", "content": "Return JSON only."}, | |
| {"role": "user", "content": [ | |
| {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": encoded}}, | |
| {"type": "text", "text": "Describe this image."}, | |
| ]}, | |
| ], | |
| } | |
| system, text, img_bytes = _extract_input(payload["messages"]) | |
| print(f"OK — system:{len(system)}c text:{text} image:{len(img_bytes)}b") | |
| print("Deploy with: modal deploy modal_app.py") |