sica-bridge 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sica_bridge-0.1.0/LICENSE +21 -0
- sica_bridge-0.1.0/PKG-INFO +201 -0
- sica_bridge-0.1.0/README.md +183 -0
- sica_bridge-0.1.0/pyproject.toml +38 -0
- sica_bridge-0.1.0/setup.cfg +4 -0
- sica_bridge-0.1.0/src/__init__.py +1 -0
- sica_bridge-0.1.0/src/sica_bridge/assess/__init__.py +4 -0
- sica_bridge-0.1.0/src/sica_bridge/assess/component_assessor.py +77 -0
- sica_bridge-0.1.0/src/sica_bridge/assess/event_aggregator.py +20 -0
- sica_bridge-0.1.0/src/sica_bridge/llm/__init__.py +3 -0
- sica_bridge-0.1.0/src/sica_bridge/llm/client.py +47 -0
- sica_bridge-0.1.0/src/sica_bridge/llm/openai_client.py +59 -0
- sica_bridge-0.1.0/src/sica_bridge/llm/prompt_builder.py +43 -0
- sica_bridge-0.1.0/src/sica_bridge/registry/__init__.py +3 -0
- sica_bridge-0.1.0/src/sica_bridge/registry/components.py +73 -0
- sica_bridge-0.1.0/src/sica_bridge/resources/__init__.py +3 -0
- sica_bridge-0.1.0/src/sica_bridge/resources/loader.py +108 -0
- sica_bridge-0.1.0/src/sica_bridge/schemas/__init__.py +3 -0
- sica_bridge-0.1.0/src/sica_bridge/schemas/outputs.py +29 -0
- sica_bridge-0.1.0/src/sica_bridge/utils/__init__.py +3 -0
- sica_bridge-0.1.0/src/sica_bridge/utils/io.py +36 -0
- sica_bridge-0.1.0/src/sica_bridge.egg-info/PKG-INFO +201 -0
- sica_bridge-0.1.0/src/sica_bridge.egg-info/SOURCES.txt +32 -0
- sica_bridge-0.1.0/src/sica_bridge.egg-info/dependency_links.txt +1 -0
- sica_bridge-0.1.0/src/sica_bridge.egg-info/requires.txt +8 -0
- sica_bridge-0.1.0/src/sica_bridge.egg-info/top_level.txt +2 -0
- sica_bridge-0.1.0/tests/test_component_assessor.py +17 -0
- sica_bridge-0.1.0/tests/test_event_aggregator.py +20 -0
- sica_bridge-0.1.0/tests/test_llm_client_fake.py +7 -0
- sica_bridge-0.1.0/tests/test_multi_photo.py +18 -0
- sica_bridge-0.1.0/tests/test_prompt_builder.py +9 -0
- sica_bridge-0.1.0/tests/test_registry.py +17 -0
- sica_bridge-0.1.0/tests/test_resource_loading.py +15 -0
- sica_bridge-0.1.0/tests/test_schema_validation.py +25 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Issac Pang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sica-bridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Structure Investigation and Condition Assessment for Bridges (post-earthquake visual inspection)
|
|
5
|
+
Author: Issac K.T. Pang
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0
|
|
12
|
+
Requires-Dist: openai>=1.0.0
|
|
13
|
+
Requires-Dist: python-dotenv>=1.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# sica-bridge
|
|
20
|
+
|
|
21
|
+
<img src="image/sica_logo.svg" alt="SICA logo" width="420"/>
|
|
22
|
+
|
|
23
|
+
**SICA** = **S**tructure **I**nvestigation and **C**ondition **A**ssessment.
|
|
24
|
+
|
|
25
|
+
`sica-bridge` is a Python package that uses a vision-capable LLM to perform **post-earthquake visual inspection** from photos and to **support rapid decision-making** for bridge safety assessment.
|
|
26
|
+
It outputs an **R-state** (R1–R4) and a **short reason** per photo, then aggregates results to component-level and event-level summaries that can be used to **prioritize inspections, closures, and follow-up actions**.
|
|
27
|
+
|
|
28
|
+
> Current scope: 4 component categories (approaches, columns, joints/hinges, abutments/wingwalls/shear keys).
|
|
29
|
+
|
|
30
|
+
## R-states (Action Plan)
|
|
31
|
+
|
|
32
|
+
- **R1** — Open
|
|
33
|
+
- **R2** — Open but inspection needed
|
|
34
|
+
- **R3** — Close but inspection needed
|
|
35
|
+
- **R4** — Close immediately
|
|
36
|
+
|
|
37
|
+
## Repository layout
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
.
|
|
41
|
+
├─ src/sica_bridge/
|
|
42
|
+
│ ├─ assess/ # photo-level assessment + aggregation helpers
|
|
43
|
+
│ ├─ llm/ # LLM client interface + prompt builder
|
|
44
|
+
│ ├─ registry/ # component registry (ids, display names, filenames)
|
|
45
|
+
│ ├─ resources/ # loaders for prompts/rubrics/reference images
|
|
46
|
+
│ ├─ schemas/ # Pydantic output schemas (RState, ComponentAssessment, EventAssessment)
|
|
47
|
+
│ ├─ utils/ # helpers (e.g., robust JSON extraction)
|
|
48
|
+
│ └─ gui/ # demo UI
|
|
49
|
+
├─ prompts/
|
|
50
|
+
│ ├─ system.md
|
|
51
|
+
│ └─ components/ # per-component prompt templates (.md)
|
|
52
|
+
├─ rubrics/ # per-component rubrics (.yaml)
|
|
53
|
+
├─ assets/
|
|
54
|
+
│ └─ damage_examples/ # optional reference images used in prompts
|
|
55
|
+
│ └─ <component_id>/
|
|
56
|
+
│ ├─ meta.yaml # optional captions & ordering
|
|
57
|
+
│ └─ *.jpg|*.png
|
|
58
|
+
└─ tests/ # pytest unit tests
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
### Requirements
|
|
64
|
+
- Python **>= 3.10**
|
|
65
|
+
|
|
66
|
+
### From source (recommended for development)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install -e .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Dev dependencies
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install -e ".[dev]"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quickstart: run the demo
|
|
79
|
+
|
|
80
|
+
The repo includes a default UI and you can have a quick local deployment for research or testing:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
python src/sica_bridge/gui/webserver.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then open the local server URL and upload one or more photos for each component category. Please don't forget to set up the OpenAI or other LLM API key for the testing. If you are planning to use your own computer vision models, please set up the GPU server for testing. Go to [Adding a new LLM provider](README_new_llm_section.md) for further information.
|
|
87
|
+
|
|
88
|
+
## How it works (pipeline)
|
|
89
|
+
|
|
90
|
+
For each uploaded image:
|
|
91
|
+
|
|
92
|
+
1. **Load reference images** (optional, preset) for the component from:
|
|
93
|
+
- `assets/damage_examples/<component_id>/`
|
|
94
|
+
2. **Build a prompt**:
|
|
95
|
+
- `prompts/system.md`
|
|
96
|
+
- `prompts/components/<component_id>.md`
|
|
97
|
+
- `rubrics/<component_id>.yaml` (parsed into JSON and embedded)
|
|
98
|
+
- **Pydantic JSON schema** for `ComponentAssessment` (the output contract)
|
|
99
|
+
3. Call a `VisionLLMClient` to produce **JSON-only output**.
|
|
100
|
+
4. Parse and validate the JSON into `ComponentAssessment`.
|
|
101
|
+
5. Aggregate:
|
|
102
|
+
- component-level: worst-case (max severity) across that component’s photos
|
|
103
|
+
- event-level: worst-case (max severity) across all component assessments
|
|
104
|
+
|
|
105
|
+
### Schemas
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from sica_bridge.schemas import RState, ComponentAssessment, EventAssessment
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- `RState`: Enum of `R1`, `R2`, `R3`, `R4`
|
|
112
|
+
- `ComponentAssessment`:
|
|
113
|
+
- `component_id: str`
|
|
114
|
+
- `r_state: RState`
|
|
115
|
+
- `reason: str`
|
|
116
|
+
- `notes: Optional[str]`
|
|
117
|
+
- `EventAssessment`:
|
|
118
|
+
- `overall_r_state: RState`
|
|
119
|
+
- `components: list[ComponentAssessment]`
|
|
120
|
+
|
|
121
|
+
### Assessment
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from sica_bridge.assess import assess_component, assess_component_many, aggregate_component, aggregate_event
|
|
125
|
+
from sica_bridge.llm.openai_client import OpenAIVisionClient
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Assess one photo**
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
client = OpenAIVisionClient()
|
|
132
|
+
out = assess_component(
|
|
133
|
+
component_id="columns",
|
|
134
|
+
image_bytes=open("my_photo.jpg", "rb").read(),
|
|
135
|
+
mime_type="image/jpeg",
|
|
136
|
+
filename="my_photo.jpg",
|
|
137
|
+
client=client,
|
|
138
|
+
)
|
|
139
|
+
print(out.r_state, out.reason)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Assess many photos for one component**
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
photos = [
|
|
146
|
+
(open("a.jpg","rb").read(), "a.jpg", "image/jpeg"),
|
|
147
|
+
(open("b.jpg","rb").read(), "b.jpg", "image/jpeg"),
|
|
148
|
+
]
|
|
149
|
+
results = assess_component_many(component_id="columns", photos=photos, client=client)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Aggregate component and event states**
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
overall_component = aggregate_component(results) # -> RState
|
|
156
|
+
event = aggregate_event(results) # -> EventAssessment (worst-case across provided items)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## LLM clients
|
|
162
|
+
|
|
163
|
+
### Interface
|
|
164
|
+
|
|
165
|
+
Implement this provider-agnostic interface:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from sica_bridge.llm.client import VisionLLMClient, VisionInput
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`VisionLLMClient.complete_json(prompt: str, images: list[VisionInput]) -> str`
|
|
172
|
+
should return a **JSON object** as plain text (no markdown), matching the schema.
|
|
173
|
+
|
|
174
|
+
### OpenAI implementation
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from sica_bridge.llm.openai_client import OpenAIVisionClient
|
|
178
|
+
client = OpenAIVisionClient(model="gpt-5.2")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
In case you want to use your own computer vision models or other LLMs, please also check [Adding a new LLM provider](README_new_llm_section.md)
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Adding / editing components
|
|
186
|
+
|
|
187
|
+
Component categories are defined in:
|
|
188
|
+
|
|
189
|
+
- `src/sica_bridge/registry/components.py`
|
|
190
|
+
|
|
191
|
+
Each component has:
|
|
192
|
+
- `id` (stable key used across prompts, rubrics, schemas, GUI)
|
|
193
|
+
- `rubric_filename` in `rubrics/`
|
|
194
|
+
- `prompt_filename` in `prompts/`
|
|
195
|
+
|
|
196
|
+
To add a new component, add a new `ComponentSpec` entry and create:
|
|
197
|
+
- `rubrics/<new_id>.yaml`
|
|
198
|
+
- `prompts/components/<new_id>.md`
|
|
199
|
+
- (optional) `assets/damage_examples/<new_id>/...`
|
|
200
|
+
|
|
201
|
+
---
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# sica-bridge
|
|
2
|
+
|
|
3
|
+
<img src="image/sica_logo.svg" alt="SICA logo" width="420"/>
|
|
4
|
+
|
|
5
|
+
**SICA** = **S**tructure **I**nvestigation and **C**ondition **A**ssessment.
|
|
6
|
+
|
|
7
|
+
`sica-bridge` is a Python package that uses a vision-capable LLM to perform **post-earthquake visual inspection** from photos and to **support rapid decision-making** for bridge safety assessment.
|
|
8
|
+
It outputs an **R-state** (R1–R4) and a **short reason** per photo, then aggregates results to component-level and event-level summaries that can be used to **prioritize inspections, closures, and follow-up actions**.
|
|
9
|
+
|
|
10
|
+
> Current scope: 4 component categories (approaches, columns, joints/hinges, abutments/wingwalls/shear keys).
|
|
11
|
+
|
|
12
|
+
## R-states (Action Plan)
|
|
13
|
+
|
|
14
|
+
- **R1** — Open
|
|
15
|
+
- **R2** — Open but inspection needed
|
|
16
|
+
- **R3** — Close but inspection needed
|
|
17
|
+
- **R4** — Close immediately
|
|
18
|
+
|
|
19
|
+
## Repository layout
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
.
|
|
23
|
+
├─ src/sica_bridge/
|
|
24
|
+
│ ├─ assess/ # photo-level assessment + aggregation helpers
|
|
25
|
+
│ ├─ llm/ # LLM client interface + prompt builder
|
|
26
|
+
│ ├─ registry/ # component registry (ids, display names, filenames)
|
|
27
|
+
│ ├─ resources/ # loaders for prompts/rubrics/reference images
|
|
28
|
+
│ ├─ schemas/ # Pydantic output schemas (RState, ComponentAssessment, EventAssessment)
|
|
29
|
+
│ ├─ utils/ # helpers (e.g., robust JSON extraction)
|
|
30
|
+
│ └─ gui/ # demo UI
|
|
31
|
+
├─ prompts/
|
|
32
|
+
│ ├─ system.md
|
|
33
|
+
│ └─ components/ # per-component prompt templates (.md)
|
|
34
|
+
├─ rubrics/ # per-component rubrics (.yaml)
|
|
35
|
+
├─ assets/
|
|
36
|
+
│ └─ damage_examples/ # optional reference images used in prompts
|
|
37
|
+
│ └─ <component_id>/
|
|
38
|
+
│ ├─ meta.yaml # optional captions & ordering
|
|
39
|
+
│ └─ *.jpg|*.png
|
|
40
|
+
└─ tests/ # pytest unit tests
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
### Requirements
|
|
46
|
+
- Python **>= 3.10**
|
|
47
|
+
|
|
48
|
+
### From source (recommended for development)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Dev dependencies
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install -e ".[dev]"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quickstart: run the demo
|
|
61
|
+
|
|
62
|
+
The repo includes a default UI and you can have a quick local deployment for research or testing:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python src/sica_bridge/gui/webserver.py
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then open the local server URL and upload one or more photos for each component category. Please don't forget to set up the OpenAI or other LLM API key for the testing. If you are planning to use your own computer vision models, please set up the GPU server for testing. Go to [Adding a new LLM provider](README_new_llm_section.md) for further information.
|
|
69
|
+
|
|
70
|
+
## How it works (pipeline)
|
|
71
|
+
|
|
72
|
+
For each uploaded image:
|
|
73
|
+
|
|
74
|
+
1. **Load reference images** (optional, preset) for the component from:
|
|
75
|
+
- `assets/damage_examples/<component_id>/`
|
|
76
|
+
2. **Build a prompt**:
|
|
77
|
+
- `prompts/system.md`
|
|
78
|
+
- `prompts/components/<component_id>.md`
|
|
79
|
+
- `rubrics/<component_id>.yaml` (parsed into JSON and embedded)
|
|
80
|
+
- **Pydantic JSON schema** for `ComponentAssessment` (the output contract)
|
|
81
|
+
3. Call a `VisionLLMClient` to produce **JSON-only output**.
|
|
82
|
+
4. Parse and validate the JSON into `ComponentAssessment`.
|
|
83
|
+
5. Aggregate:
|
|
84
|
+
- component-level: worst-case (max severity) across that component’s photos
|
|
85
|
+
- event-level: worst-case (max severity) across all component assessments
|
|
86
|
+
|
|
87
|
+
### Schemas
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from sica_bridge.schemas import RState, ComponentAssessment, EventAssessment
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- `RState`: Enum of `R1`, `R2`, `R3`, `R4`
|
|
94
|
+
- `ComponentAssessment`:
|
|
95
|
+
- `component_id: str`
|
|
96
|
+
- `r_state: RState`
|
|
97
|
+
- `reason: str`
|
|
98
|
+
- `notes: Optional[str]`
|
|
99
|
+
- `EventAssessment`:
|
|
100
|
+
- `overall_r_state: RState`
|
|
101
|
+
- `components: list[ComponentAssessment]`
|
|
102
|
+
|
|
103
|
+
### Assessment
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from sica_bridge.assess import assess_component, assess_component_many, aggregate_component, aggregate_event
|
|
107
|
+
from sica_bridge.llm.openai_client import OpenAIVisionClient
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Assess one photo**
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
client = OpenAIVisionClient()
|
|
114
|
+
out = assess_component(
|
|
115
|
+
component_id="columns",
|
|
116
|
+
image_bytes=open("my_photo.jpg", "rb").read(),
|
|
117
|
+
mime_type="image/jpeg",
|
|
118
|
+
filename="my_photo.jpg",
|
|
119
|
+
client=client,
|
|
120
|
+
)
|
|
121
|
+
print(out.r_state, out.reason)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Assess many photos for one component**
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
photos = [
|
|
128
|
+
(open("a.jpg","rb").read(), "a.jpg", "image/jpeg"),
|
|
129
|
+
(open("b.jpg","rb").read(), "b.jpg", "image/jpeg"),
|
|
130
|
+
]
|
|
131
|
+
results = assess_component_many(component_id="columns", photos=photos, client=client)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Aggregate component and event states**
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
overall_component = aggregate_component(results) # -> RState
|
|
138
|
+
event = aggregate_event(results) # -> EventAssessment (worst-case across provided items)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## LLM clients
|
|
144
|
+
|
|
145
|
+
### Interface
|
|
146
|
+
|
|
147
|
+
Implement this provider-agnostic interface:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from sica_bridge.llm.client import VisionLLMClient, VisionInput
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`VisionLLMClient.complete_json(prompt: str, images: list[VisionInput]) -> str`
|
|
154
|
+
should return a **JSON object** as plain text (no markdown), matching the schema.
|
|
155
|
+
|
|
156
|
+
### OpenAI implementation
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from sica_bridge.llm.openai_client import OpenAIVisionClient
|
|
160
|
+
client = OpenAIVisionClient(model="gpt-5.2")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
In case you want to use your own computer vision models or other LLMs, please also check [Adding a new LLM provider](README_new_llm_section.md)
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Adding / editing components
|
|
168
|
+
|
|
169
|
+
Component categories are defined in:
|
|
170
|
+
|
|
171
|
+
- `src/sica_bridge/registry/components.py`
|
|
172
|
+
|
|
173
|
+
Each component has:
|
|
174
|
+
- `id` (stable key used across prompts, rubrics, schemas, GUI)
|
|
175
|
+
- `rubric_filename` in `rubrics/`
|
|
176
|
+
- `prompt_filename` in `prompts/`
|
|
177
|
+
|
|
178
|
+
To add a new component, add a new `ComponentSpec` entry and create:
|
|
179
|
+
- `rubrics/<new_id>.yaml`
|
|
180
|
+
- `prompts/components/<new_id>.md`
|
|
181
|
+
- (optional) `assets/damage_examples/<new_id>/...`
|
|
182
|
+
|
|
183
|
+
---
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sica-bridge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Structure Investigation and Condition Assessment for Bridges (post-earthquake visual inspection)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Issac K.T. Pang" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"pydantic>=2.0",
|
|
15
|
+
"pyyaml>=6.0",
|
|
16
|
+
"openai>=1.0.0",
|
|
17
|
+
"python-dotenv>=1.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=7.0",
|
|
23
|
+
"ruff>=0.5",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
package-dir = {"" = "src"}
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-data]
|
|
30
|
+
# include non-python assets if/when we move rubrics/prompts into src later
|
|
31
|
+
sica_bridge = ["py.typed", "data/**/*"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 100
|
|
38
|
+
target-version = "py310"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sica_bridge.llm import render_prompt
|
|
4
|
+
from sica_bridge.llm.client import VisionInput, VisionLLMClient
|
|
5
|
+
from sica_bridge.schemas import ComponentAssessment
|
|
6
|
+
from sica_bridge.utils import extract_json_object
|
|
7
|
+
from sica_bridge.resources import load_reference_images
|
|
8
|
+
from typing import Iterable
|
|
9
|
+
from sica_bridge.schemas import RState
|
|
10
|
+
|
|
11
|
+
# severity order (higher = worse)
|
|
12
|
+
_SEVERITY = {RState.R1: 1, RState.R2: 2, RState.R3: 3, RState.R4: 4}
|
|
13
|
+
|
|
14
|
+
def assess_component(
|
|
15
|
+
*,
|
|
16
|
+
component_id: str,
|
|
17
|
+
image_bytes: bytes,
|
|
18
|
+
client: VisionLLMClient,
|
|
19
|
+
mime_type: str = "image/jpeg",
|
|
20
|
+
filename: str | None = None,
|
|
21
|
+
reference_limit: int = 4, # optional tuning knob
|
|
22
|
+
) -> ComponentAssessment:
|
|
23
|
+
"""
|
|
24
|
+
Assess ONE inspection photo.
|
|
25
|
+
Preset reference images are auto-loaded from the codebase.
|
|
26
|
+
"""
|
|
27
|
+
query_image = VisionInput(
|
|
28
|
+
image_bytes=image_bytes,
|
|
29
|
+
mime_type=mime_type,
|
|
30
|
+
filename=filename,
|
|
31
|
+
role="query",
|
|
32
|
+
caption="Inspection photo",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
refs = load_reference_images(component_id, limit=reference_limit) # preset only
|
|
36
|
+
images = [query_image, *refs]
|
|
37
|
+
|
|
38
|
+
prompt = render_prompt(component_id, images=images)
|
|
39
|
+
raw = client.complete_json(prompt=prompt, images=images)
|
|
40
|
+
|
|
41
|
+
data = extract_json_object(raw)
|
|
42
|
+
data["component_id"] = component_id # enforce consistency
|
|
43
|
+
return ComponentAssessment.model_validate(data)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def assess_component_many(
|
|
47
|
+
*,
|
|
48
|
+
component_id: str,
|
|
49
|
+
photos: Iterable[tuple[bytes, str | None, str]], # (image_bytes, filename, mime_type)
|
|
50
|
+
client,
|
|
51
|
+
) -> list[ComponentAssessment]:
|
|
52
|
+
"""
|
|
53
|
+
Issac: assess inspection photos one-by-one.
|
|
54
|
+
Each photo returns its own R_state + reason.
|
|
55
|
+
"""
|
|
56
|
+
results: list[ComponentAssessment] = []
|
|
57
|
+
for idx, (b, fname, mt) in enumerate(photos, start=1):
|
|
58
|
+
a = assess_component(
|
|
59
|
+
component_id=component_id,
|
|
60
|
+
image_bytes=b,
|
|
61
|
+
filename=fname,
|
|
62
|
+
mime_type=mt,
|
|
63
|
+
client=client,
|
|
64
|
+
)
|
|
65
|
+
# optional traceability (doesn't change required fields)
|
|
66
|
+
a = a.model_copy(update={"notes": f"photo_index={idx}, filename={fname}"})
|
|
67
|
+
results.append(a)
|
|
68
|
+
return results
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def aggregate_component(assessments: list[ComponentAssessment]) -> RState:
|
|
72
|
+
"""
|
|
73
|
+
Worst-case rollup for ONE component (across multiple photos).
|
|
74
|
+
"""
|
|
75
|
+
if not assessments:
|
|
76
|
+
raise ValueError("No photo assessments for this component.")
|
|
77
|
+
return max(assessments, key=lambda a: _SEVERITY[a.r_state]).r_state
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sica_bridge.schemas import ComponentAssessment, EventAssessment, RState
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# severity order (higher = worse)
|
|
7
|
+
_SEVERITY = {
|
|
8
|
+
RState.R1: 1,
|
|
9
|
+
RState.R2: 2,
|
|
10
|
+
RState.R3: 3,
|
|
11
|
+
RState.R4: 4,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def aggregate_event(components: list[ComponentAssessment]) -> EventAssessment:
|
|
16
|
+
if not components:
|
|
17
|
+
raise ValueError("No component assessments provided.")
|
|
18
|
+
|
|
19
|
+
overall = max(components, key=lambda c: _SEVERITY[c.r_state]).r_state
|
|
20
|
+
return EventAssessment(overall_r_state=overall, components=components)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Optional, Literal
|
|
6
|
+
|
|
7
|
+
ImageRole = Literal["query", "reference"]
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class VisionInput:
|
|
11
|
+
"""
|
|
12
|
+
Represents one image to analyze.
|
|
13
|
+
The model will receive images in the given order.
|
|
14
|
+
"""
|
|
15
|
+
image_bytes: bytes
|
|
16
|
+
mime_type: str = "image/jpeg"
|
|
17
|
+
filename: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
# Issac: help the prompt explain what each image is
|
|
20
|
+
role: ImageRole = "query" # "query" or "reference"
|
|
21
|
+
caption: Optional[str] = None # e.g., "Example: diagonal shear cracking"
|
|
22
|
+
|
|
23
|
+
class VisionLLMClient(abc.ABC):
|
|
24
|
+
"""
|
|
25
|
+
Provider-agnostic interface.
|
|
26
|
+
Implementations will call OpenAI / Anthropic / etc.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@abc.abstractmethod
|
|
30
|
+
def complete_json(self, *, prompt: str, images: list[VisionInput]) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Returns the model's raw text output (expected to be JSON text).
|
|
33
|
+
"""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FakeVisionLLMClient(VisionLLMClient):
|
|
38
|
+
"""
|
|
39
|
+
Deterministic fake for tests/dev without network.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, json_text: str):
|
|
43
|
+
self._json_text = json_text
|
|
44
|
+
|
|
45
|
+
def complete_json(self, *, prompt: str, images: VisionInput) -> str:
|
|
46
|
+
# ignore prompt/image; return deterministic stub
|
|
47
|
+
return self._json_text
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from openai import OpenAI
|
|
9
|
+
|
|
10
|
+
from sica_bridge.llm.client import VisionInput, VisionLLMClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _to_data_url(image_bytes: bytes, mime_type: str) -> str:
|
|
14
|
+
b64 = base64.b64encode(image_bytes).decode("ascii")
|
|
15
|
+
return f"data:{mime_type};base64,{b64}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class OpenAIVisionClient(VisionLLMClient):
|
|
20
|
+
"""
|
|
21
|
+
OpenAI Responses API implementation.
|
|
22
|
+
|
|
23
|
+
API key is read from environment variable OPENAI_API_KEY.
|
|
24
|
+
"""
|
|
25
|
+
model: str = "gpt-5.2"
|
|
26
|
+
detail: str = "auto"
|
|
27
|
+
|
|
28
|
+
def __post_init__(self) -> None:
|
|
29
|
+
api_key = os.getenv("OPENAI_API_KEY")
|
|
30
|
+
if not api_key:
|
|
31
|
+
raise RuntimeError(
|
|
32
|
+
"OPENAI_API_KEY environment variable is not set. "
|
|
33
|
+
"Set it before running the application."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self._client = OpenAI(api_key=api_key)
|
|
37
|
+
|
|
38
|
+
self.model = os.getenv("SICA_OPENAI_MODEL", self.model)
|
|
39
|
+
|
|
40
|
+
def _build_input(self, prompt: str, images: list[VisionInput]) -> list[Dict[str, Any]]:
|
|
41
|
+
content: List[Dict[str, Any]] = [{"type": "input_text", "text": prompt}]
|
|
42
|
+
|
|
43
|
+
for im in images:
|
|
44
|
+
content.append(
|
|
45
|
+
{
|
|
46
|
+
"type": "input_image",
|
|
47
|
+
"image_url": _to_data_url(im.image_bytes, im.mime_type),
|
|
48
|
+
"detail": self.detail,
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return [{"role": "user", "content": content}]
|
|
53
|
+
|
|
54
|
+
def complete_json(self, *, prompt: str, images: list[VisionInput]) -> str:
|
|
55
|
+
resp = self._client.responses.create(
|
|
56
|
+
model=self.model,
|
|
57
|
+
input=self._build_input(prompt, images),
|
|
58
|
+
)
|
|
59
|
+
return (resp.output_text or "").strip()
|