ZahraFatima commited on
Commit
e2c486b
·
verified ·
1 Parent(s): 34434f8

Update vlm_caption.py

Browse files
Files changed (1) hide show
  1. vlm_caption.py +83 -33
vlm_caption.py CHANGED
@@ -1,17 +1,20 @@
1
  """
2
- VLM captioning: image + structured YOLO detections -> safety-aware observation.
3
 
4
- This is the LANGUAGE half of the pipeline. The VLM receives:
5
- - the image itself (so it can describe scene/context YOLO can't)
6
- - the deterministic detections + compliance findings from detector.py
7
 
8
- It is instructed to treat the detector's compliance findings as the source of
9
- truth for PPE claims, and to use the image only for context/description. That
10
- boundary is what keeps the safety judgement auditable.
11
 
12
- Supports MULTIPLE models behind one interface:
13
- - Claude models (Anthropic SDK) -> require ANTHROPIC_API_KEY
14
- - Qwen3-VL models (DashScope, OpenAI-compatible) -> require DASHSCOPE_API_KEY
 
 
 
 
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
- SYSTEM_PROMPT = """You are a workplace-safety observation assistant for the AiGiS platform.
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- Return ONLY a JSON object with these keys, nothing else:
68
- "caption": one-sentence description of the scene,
69
- "safety_observation": 2-4 sentences on PPE compliance and visible hazards,
70
- "ppe_summary": object mapping each assessed body part to "present"/"missing"/"uncertain",
71
- "severity": one of "none", "low", "medium", "high" — your read of overall risk.
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=SYSTEM_PROMPT,
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": SYSTEM_PROMPT},
159
  {
160
  "role": "user",
161
  "content": [
@@ -181,37 +213,55 @@ _BACKENDS = {
181
  # ---------------------------------------------------------------------------
182
  # Public entry points
183
  # ---------------------------------------------------------------------------
184
- def caption(image_path: str, detection_result: dict, model: str = DEFAULT_MODEL) -> dict:
 
 
 
 
 
185
  """
186
- detection_result: the dict from detector.DetectionResult.to_dict()
187
  model: registry key ("sonnet", "qwen-32b", ...) or a raw model id.
188
- Returns the parsed JSON dict from the VLM.
 
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(media_type: str, b64: str, detection_result: dict, model: str) -> dict:
 
 
 
 
 
 
208
  key, provider, model_id = _resolve(model)
209
- user_text = _build_user_text(detection_result)
 
 
 
 
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
- return result
 
 
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