Spaces:
Sleeping
Sleeping
Update vlm_caption.py
Browse files- vlm_caption.py +83 -33
vlm_caption.py
CHANGED
|
@@ -1,17 +1,20 @@
|
|
| 1 |
"""
|
| 2 |
-
VLM captioning: image +
|
| 3 |
|
| 4 |
-
This is the LANGUAGE half of the pipeline.
|
| 5 |
-
|
| 6 |
-
- the deterministic detections + compliance findings from detector.py
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
Add a new model by adding one entry to MODEL_REGISTRY.
|
| 17 |
"""
|
|
@@ -47,7 +50,20 @@ DEFAULT_MODEL = "sonnet"
|
|
| 47 |
DASHSCOPE_BASE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
| 48 |
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
You are given (1) an image of a work environment and (2) structured detections
|
| 53 |
from a deterministic object-detection model, including PPE-compliance findings.
|
|
@@ -62,14 +78,28 @@ Rules:
|
|
| 62 |
that the detector does not cover.
|
| 63 |
- Do not invent objects or people not supported by the detections or clearly
|
| 64 |
visible in the image.
|
| 65 |
-
- Be concise, factual, and neutral. This is a safety record, not marketing copy.
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
|
| 75 |
# ---------------------------------------------------------------------------
|
|
@@ -94,7 +124,9 @@ def _resolve(model: str) -> tuple[str, str, str]:
|
|
| 94 |
return model, provider, model
|
| 95 |
|
| 96 |
|
| 97 |
-
def _build_user_text(detection_result: dict) -> str:
|
|
|
|
|
|
|
| 98 |
return (
|
| 99 |
"Structured detector output (authoritative for PPE claims):\n"
|
| 100 |
+ json.dumps(detection_result, indent=2)
|
|
@@ -114,14 +146,14 @@ def _parse(text: str) -> dict:
|
|
| 114 |
# ---------------------------------------------------------------------------
|
| 115 |
# Provider back-ends
|
| 116 |
# ---------------------------------------------------------------------------
|
| 117 |
-
def _caption_anthropic(model_id, media_type, b64, user_text) -> dict:
|
| 118 |
import anthropic
|
| 119 |
|
| 120 |
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
|
| 121 |
msg = client.messages.create(
|
| 122 |
model=model_id,
|
| 123 |
max_tokens=1024,
|
| 124 |
-
system=
|
| 125 |
messages=[
|
| 126 |
{
|
| 127 |
"role": "user",
|
|
@@ -143,7 +175,7 @@ def _caption_anthropic(model_id, media_type, b64, user_text) -> dict:
|
|
| 143 |
return _parse(text)
|
| 144 |
|
| 145 |
|
| 146 |
-
def _caption_dashscope(model_id, media_type, b64, user_text) -> dict:
|
| 147 |
# DashScope is OpenAI-compatible; the OpenAI SDK is the client.
|
| 148 |
from openai import OpenAI
|
| 149 |
|
|
@@ -155,7 +187,7 @@ def _caption_dashscope(model_id, media_type, b64, user_text) -> dict:
|
|
| 155 |
model=model_id,
|
| 156 |
max_tokens=1024,
|
| 157 |
messages=[
|
| 158 |
-
{"role": "system", "content":
|
| 159 |
{
|
| 160 |
"role": "user",
|
| 161 |
"content": [
|
|
@@ -181,37 +213,55 @@ _BACKENDS = {
|
|
| 181 |
# ---------------------------------------------------------------------------
|
| 182 |
# Public entry points
|
| 183 |
# ---------------------------------------------------------------------------
|
| 184 |
-
def caption(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
"""
|
| 186 |
-
detection_result: the dict from detector.DetectionResult.to_dict()
|
| 187 |
model: registry key ("sonnet", "qwen-32b", ...) or a raw model id.
|
| 188 |
-
|
|
|
|
| 189 |
"""
|
| 190 |
media_type, b64 = _encode_image(image_path)
|
| 191 |
-
return _caption_from_b64(media_type, b64, detection_result, model)
|
| 192 |
|
| 193 |
|
| 194 |
def caption_bytes(
|
| 195 |
image_bytes: bytes,
|
| 196 |
media_type: str,
|
| 197 |
-
detection_result: dict,
|
| 198 |
model: str = DEFAULT_MODEL,
|
|
|
|
| 199 |
) -> dict:
|
| 200 |
"""
|
| 201 |
Same as caption(), but for an image already in memory instead of one on disk.
|
| 202 |
"""
|
| 203 |
b64 = base64.standard_b64encode(image_bytes).decode()
|
| 204 |
-
return _caption_from_b64(media_type, b64, detection_result, model)
|
| 205 |
|
| 206 |
|
| 207 |
-
def _caption_from_b64(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
key, provider, model_id = _resolve(model)
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
backend = _BACKENDS.get(provider)
|
| 212 |
if backend is None:
|
| 213 |
raise ValueError(f"Unknown provider '{provider}' for model '{model}'")
|
| 214 |
|
| 215 |
-
result = backend(model_id, media_type, b64, user_text)
|
| 216 |
result["_model"] = key
|
| 217 |
-
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
VLM captioning: image [+ optional YOLO detections] -> safety-aware observation.
|
| 3 |
|
| 4 |
+
This is the LANGUAGE half of the pipeline. It supports MULTIPLE models behind a
|
| 5 |
+
single interface, and two modes:
|
|
|
|
| 6 |
|
| 7 |
+
1. WITH detector (default): the VLM receives the image AND the deterministic
|
| 8 |
+
detections + compliance findings, and is told to treat the detector's
|
| 9 |
+
compliance findings as authoritative. This is the AiGiS design.
|
| 10 |
|
| 11 |
+
2. IMAGE-ONLY (use_detector=False): the VLM receives the image alone, with no
|
| 12 |
+
detector JSON, and judges PPE from the image itself. This is the baseline
|
| 13 |
+
that shows what the detector adds.
|
| 14 |
+
|
| 15 |
+
Providers:
|
| 16 |
+
- Claude models (Anthropic SDK) require ANTHROPIC_API_KEY
|
| 17 |
+
- Qwen3-VL models (DashScope, OpenAI-compatible) require DASHSCOPE_API_KEY
|
| 18 |
|
| 19 |
Add a new model by adding one entry to MODEL_REGISTRY.
|
| 20 |
"""
|
|
|
|
| 50 |
DASHSCOPE_BASE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
| 51 |
|
| 52 |
|
| 53 |
+
# ---------------------------------------------------------------------------
|
| 54 |
+
# Prompts
|
| 55 |
+
# ---------------------------------------------------------------------------
|
| 56 |
+
# Shared output contract for both modes.
|
| 57 |
+
_SYSTEM_COMMON = """Return ONLY a JSON object with these keys, nothing else:
|
| 58 |
+
"caption": one-sentence description of the scene,
|
| 59 |
+
"safety_observation": 2-4 sentences on PPE compliance and visible hazards,
|
| 60 |
+
"ppe_summary": object mapping each assessed body part to "present"/"missing"/"uncertain",
|
| 61 |
+
"severity": one of "none", "low", "medium", "high" — your read of overall risk.
|
| 62 |
+
|
| 63 |
+
Be concise, factual, and neutral. This is a safety record, not marketing copy."""
|
| 64 |
+
|
| 65 |
+
# Mode 1: detector output provided and authoritative.
|
| 66 |
+
SYSTEM_WITH_DETECTOR = """You are a workplace-safety observation assistant for the AiGiS platform.
|
| 67 |
|
| 68 |
You are given (1) an image of a work environment and (2) structured detections
|
| 69 |
from a deterministic object-detection model, including PPE-compliance findings.
|
|
|
|
| 78 |
that the detector does not cover.
|
| 79 |
- Do not invent objects or people not supported by the detections or clearly
|
| 80 |
visible in the image.
|
|
|
|
| 81 |
|
| 82 |
+
""" + _SYSTEM_COMMON
|
| 83 |
+
|
| 84 |
+
# Mode 2: image only, no detector.
|
| 85 |
+
SYSTEM_IMAGE_ONLY = """You are a workplace-safety observation assistant for the AiGiS platform.
|
| 86 |
+
|
| 87 |
+
You are given an image of a work environment. There is no object-detection input;
|
| 88 |
+
assess the scene from the image alone.
|
| 89 |
+
|
| 90 |
+
Rules:
|
| 91 |
+
- Identify the people and safety-relevant PPE you can see (helmet, safety vest,
|
| 92 |
+
gloves, glasses/eye protection, footwear, etc.) and judge whether required PPE
|
| 93 |
+
appears present or missing for the visible activity.
|
| 94 |
+
- Note visible hazards (heights, machinery, sparks, traffic, etc.).
|
| 95 |
+
- Be explicit that these judgements come from the image alone and may need
|
| 96 |
+
on-site verification.
|
| 97 |
+
- Do not invent objects or people not clearly visible in the image.
|
| 98 |
+
|
| 99 |
+
""" + _SYSTEM_COMMON
|
| 100 |
+
|
| 101 |
+
# Backwards-compatible alias (older code referenced SYSTEM_PROMPT).
|
| 102 |
+
SYSTEM_PROMPT = SYSTEM_WITH_DETECTOR
|
| 103 |
|
| 104 |
|
| 105 |
# ---------------------------------------------------------------------------
|
|
|
|
| 124 |
return model, provider, model
|
| 125 |
|
| 126 |
|
| 127 |
+
def _build_user_text(detection_result: dict | None) -> str:
|
| 128 |
+
if detection_result is None:
|
| 129 |
+
return "Write the safety observation for this image as specified."
|
| 130 |
return (
|
| 131 |
"Structured detector output (authoritative for PPE claims):\n"
|
| 132 |
+ json.dumps(detection_result, indent=2)
|
|
|
|
| 146 |
# ---------------------------------------------------------------------------
|
| 147 |
# Provider back-ends
|
| 148 |
# ---------------------------------------------------------------------------
|
| 149 |
+
def _caption_anthropic(model_id, system, media_type, b64, user_text) -> dict:
|
| 150 |
import anthropic
|
| 151 |
|
| 152 |
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
|
| 153 |
msg = client.messages.create(
|
| 154 |
model=model_id,
|
| 155 |
max_tokens=1024,
|
| 156 |
+
system=system,
|
| 157 |
messages=[
|
| 158 |
{
|
| 159 |
"role": "user",
|
|
|
|
| 175 |
return _parse(text)
|
| 176 |
|
| 177 |
|
| 178 |
+
def _caption_dashscope(model_id, system, media_type, b64, user_text) -> dict:
|
| 179 |
# DashScope is OpenAI-compatible; the OpenAI SDK is the client.
|
| 180 |
from openai import OpenAI
|
| 181 |
|
|
|
|
| 187 |
model=model_id,
|
| 188 |
max_tokens=1024,
|
| 189 |
messages=[
|
| 190 |
+
{"role": "system", "content": system},
|
| 191 |
{
|
| 192 |
"role": "user",
|
| 193 |
"content": [
|
|
|
|
| 213 |
# ---------------------------------------------------------------------------
|
| 214 |
# Public entry points
|
| 215 |
# ---------------------------------------------------------------------------
|
| 216 |
+
def caption(
|
| 217 |
+
image_path: str,
|
| 218 |
+
detection_result: dict | None = None,
|
| 219 |
+
model: str = DEFAULT_MODEL,
|
| 220 |
+
use_detector: bool = True,
|
| 221 |
+
) -> dict:
|
| 222 |
"""
|
| 223 |
+
detection_result: the dict from detector.DetectionResult.to_dict(), or None.
|
| 224 |
model: registry key ("sonnet", "qwen-32b", ...) or a raw model id.
|
| 225 |
+
use_detector: True -> send detections, treat as authoritative.
|
| 226 |
+
False -> image-only (no detector JSON in the prompt).
|
| 227 |
"""
|
| 228 |
media_type, b64 = _encode_image(image_path)
|
| 229 |
+
return _caption_from_b64(media_type, b64, detection_result, model, use_detector)
|
| 230 |
|
| 231 |
|
| 232 |
def caption_bytes(
|
| 233 |
image_bytes: bytes,
|
| 234 |
media_type: str,
|
| 235 |
+
detection_result: dict | None = None,
|
| 236 |
model: str = DEFAULT_MODEL,
|
| 237 |
+
use_detector: bool = True,
|
| 238 |
) -> dict:
|
| 239 |
"""
|
| 240 |
Same as caption(), but for an image already in memory instead of one on disk.
|
| 241 |
"""
|
| 242 |
b64 = base64.standard_b64encode(image_bytes).decode()
|
| 243 |
+
return _caption_from_b64(media_type, b64, detection_result, model, use_detector)
|
| 244 |
|
| 245 |
|
| 246 |
+
def _caption_from_b64(
|
| 247 |
+
media_type: str,
|
| 248 |
+
b64: str,
|
| 249 |
+
detection_result: dict | None,
|
| 250 |
+
model: str,
|
| 251 |
+
use_detector: bool,
|
| 252 |
+
) -> dict:
|
| 253 |
key, provider, model_id = _resolve(model)
|
| 254 |
+
|
| 255 |
+
# Only use detector mode if we were actually given detections.
|
| 256 |
+
with_detector = use_detector and detection_result is not None
|
| 257 |
+
system = SYSTEM_WITH_DETECTOR if with_detector else SYSTEM_IMAGE_ONLY
|
| 258 |
+
user_text = _build_user_text(detection_result if with_detector else None)
|
| 259 |
|
| 260 |
backend = _BACKENDS.get(provider)
|
| 261 |
if backend is None:
|
| 262 |
raise ValueError(f"Unknown provider '{provider}' for model '{model}'")
|
| 263 |
|
| 264 |
+
result = backend(model_id, system, media_type, b64, user_text)
|
| 265 |
result["_model"] = key
|
| 266 |
+
result["_use_detector"] = with_detector
|
| 267 |
+
return result
|