pearmut 0.1.2__tar.gz → 0.1.3__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.
- {pearmut-0.1.2 → pearmut-0.1.3}/PKG-INFO +56 -5
- {pearmut-0.1.2 → pearmut-0.1.3}/README.md +55 -4
- {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/PKG-INFO +56 -5
- {pearmut-0.1.2 → pearmut-0.1.3}/pyproject.toml +1 -1
- {pearmut-0.1.2 → pearmut-0.1.3}/server/app.py +31 -5
- {pearmut-0.1.2 → pearmut-0.1.3}/server/assignment.py +138 -10
- {pearmut-0.1.2 → pearmut-0.1.3}/server/static/listwise.bundle.js +1 -1
- {pearmut-0.1.2 → pearmut-0.1.3}/server/static/listwise.html +1 -1
- pearmut-0.1.3/server/static/pointwise.bundle.js +1 -0
- pearmut-0.1.3/server/utils.py +101 -0
- pearmut-0.1.2/server/static/pointwise.bundle.js +0 -1
- pearmut-0.1.2/server/utils.py +0 -48
- {pearmut-0.1.2 → pearmut-0.1.3}/LICENSE +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/SOURCES.txt +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/dependency_links.txt +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/entry_points.txt +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/requires.txt +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/top_level.txt +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/server/cli.py +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/server/static/assets/favicon.svg +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/server/static/assets/style.css +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/server/static/dashboard.bundle.js +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/server/static/dashboard.html +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/server/static/index.html +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/server/static/pointwise.html +0 -0
- {pearmut-0.1.2 → pearmut-0.1.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pearmut
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A tool for evaluation of model outputs, primarily MT.
|
|
5
5
|
Author-email: Vilém Zouhar <vilem.zouhar@gmail.com>
|
|
6
6
|
License: apache-2.0
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
Pearmut is a **Platform for Evaluation and Reviewing of Multilingual Tasks**.
|
|
25
25
|
It evaluates model outputs, primarily translation but also various other NLP tasks.
|
|
26
|
-
Supports multimodality (text, video, audio, images) and a variety of annotation protocols (DA, ESA, MQM, paired ESA, etc).
|
|
26
|
+
Supports multimodality (text, video, audio, images) and a variety of annotation protocols ([DA](https://aclanthology.org/N15-1124/), [ESA](https://aclanthology.org/2024.wmt-1.131/), [ESA<sup>AI</sup>](https://aclanthology.org/2025.naacl-long.255/), [MQM](https://doi.org/10.1162/tacl_a_00437), paired ESA, etc).
|
|
27
27
|
|
|
28
28
|
[](https://pypi.org/project/pearmut)
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ Supports multimodality (text, video, audio, images) and a variety of annotation
|
|
|
31
31
|
|
|
32
32
|
[](https://pypi.org/project/pearmut/)
|
|
33
33
|
|
|
34
|
-
[](https://github.com/zouharvi/pearmut/actions/workflows/test.yml)
|
|
35
35
|
|
|
36
36
|
<img width="1000" alt="Screenshot of ESA/MQM interface" src="https://github.com/user-attachments/assets/f14c91a5-44d7-4248-ada9-387e95ca59d0" />
|
|
37
37
|
|
|
@@ -115,6 +115,38 @@ For the standard ones (ESA, DA, MQM), we expect each item to be a dictionary (co
|
|
|
115
115
|
... # definition of another item (document)
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
## Pre-filled Error Spans (ESA<sup>AI</sup> Support)
|
|
119
|
+
|
|
120
|
+
For workflows where you want to provide pre-filled error annotations (e.g., ESA<sup>AI</sup>), you can include an `error_spans` key in each item.
|
|
121
|
+
These spans will be loaded into the interface as existing annotations that users can review, modify, or delete.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
{
|
|
125
|
+
"src": "The quick brown fox jumps over the lazy dog.",
|
|
126
|
+
"tgt": "Rychlá hnědá liška skáče přes líného psa.",
|
|
127
|
+
"error_spans": [
|
|
128
|
+
{
|
|
129
|
+
"start_i": 0, # character index start (inclusive)
|
|
130
|
+
"end_i": 5, # character index end (inclusive)
|
|
131
|
+
"severity": "minor", # "minor", "major", "neutral", or null
|
|
132
|
+
"category": null # MQM category string or null
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"start_i": 27,
|
|
136
|
+
"end_i": 32,
|
|
137
|
+
"severity": "major",
|
|
138
|
+
"category": null
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
For **listwise** template, `error_spans` is a 2D array where each inner array corresponds to error spans for that candidate.
|
|
145
|
+
|
|
146
|
+
See [examples/esaai_prefilled.json](examples/esaai_prefilled.json) for a complete example.
|
|
147
|
+
|
|
148
|
+
## Single-stream Assignment
|
|
149
|
+
|
|
118
150
|
We also support a simple allocation where all annotators draw from the same pool (`single-stream`). Items are randomly assigned to annotators from the pool of unfinished items:
|
|
119
151
|
```python
|
|
120
152
|
{
|
|
@@ -138,7 +170,7 @@ We also support dynamic allocation of annotations (`dynamic`, not yet ⚠️), w
|
|
|
138
170
|
"campaign_id": "my campaign 6",
|
|
139
171
|
"info": {
|
|
140
172
|
"assignment": "dynamic",
|
|
141
|
-
"template": "
|
|
173
|
+
"template": "listwise",
|
|
142
174
|
"protocol_k": 5,
|
|
143
175
|
"num_users": 50,
|
|
144
176
|
},
|
|
@@ -154,6 +186,25 @@ pearmut add my_campaign_4.json
|
|
|
154
186
|
pearmut run
|
|
155
187
|
```
|
|
156
188
|
|
|
189
|
+
## Campaign options
|
|
190
|
+
|
|
191
|
+
In summary, you can select from the assignment types
|
|
192
|
+
|
|
193
|
+
- `task-based`: each user has a predefined set of items
|
|
194
|
+
- `single-stream`: all users are annotating together the same set of items
|
|
195
|
+
- `dynamic`: WIP ⚠️
|
|
196
|
+
|
|
197
|
+
and independently of that select your protocol template:
|
|
198
|
+
|
|
199
|
+
- `pointwise`: evaluate a single output given a single output
|
|
200
|
+
- `protocol_score`: ask for score 0 to 100
|
|
201
|
+
- `protocol_error_spans`: ask for highlighting error spans
|
|
202
|
+
- `protocol_error_categories`: ask for highlighting error categories
|
|
203
|
+
- `listwise`: evaluate multiple outputs at the same time given a single output ⚠️
|
|
204
|
+
- `protocol_score`: ask for score 0 to 100
|
|
205
|
+
- `protocol_error_spans`: ask for highlighting error spans
|
|
206
|
+
- `protocol_error_categories`: ask for highlighting error categories
|
|
207
|
+
|
|
157
208
|
## Campaign management
|
|
158
209
|
|
|
159
210
|
When adding new campaigns or launching pearmut, a management link is shown that gives an overview of annotator progress but also an easy access to the annotation links or resetting the task progress (no data will be lost).
|
|
@@ -170,7 +221,7 @@ An intentionally incorrect token can be shown if the annotations don't pass qual
|
|
|
170
221
|
|
|
171
222
|
We also support anything HTML-compatible both on the input and on the output.
|
|
172
223
|
This includes embedded YouTube videos, or even simple `<video ` tags that point to some resource somewhere.
|
|
173
|
-
For an example, try [examples/
|
|
224
|
+
For an example, try [examples/multimodal.json](examples/multimodal.json).
|
|
174
225
|
Tip: make sure the elements are already appropriately styled.
|
|
175
226
|
|
|
176
227
|
<img width="800" alt="Preview of multimodal elements in Pearmut" src="https://github.com/user-attachments/assets/f34a1a3e-ad95-4114-95ee-8a49e8003faf" />
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Pearmut is a **Platform for Evaluation and Reviewing of Multilingual Tasks**.
|
|
4
4
|
It evaluates model outputs, primarily translation but also various other NLP tasks.
|
|
5
|
-
Supports multimodality (text, video, audio, images) and a variety of annotation protocols (DA, ESA, MQM, paired ESA, etc).
|
|
5
|
+
Supports multimodality (text, video, audio, images) and a variety of annotation protocols ([DA](https://aclanthology.org/N15-1124/), [ESA](https://aclanthology.org/2024.wmt-1.131/), [ESA<sup>AI</sup>](https://aclanthology.org/2025.naacl-long.255/), [MQM](https://doi.org/10.1162/tacl_a_00437), paired ESA, etc).
|
|
6
6
|
|
|
7
7
|
[](https://pypi.org/project/pearmut)
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ Supports multimodality (text, video, audio, images) and a variety of annotation
|
|
|
10
10
|
|
|
11
11
|
[](https://pypi.org/project/pearmut/)
|
|
12
12
|
|
|
13
|
-
[](https://github.com/zouharvi/pearmut/actions/workflows/test.yml)
|
|
14
14
|
|
|
15
15
|
<img width="1000" alt="Screenshot of ESA/MQM interface" src="https://github.com/user-attachments/assets/f14c91a5-44d7-4248-ada9-387e95ca59d0" />
|
|
16
16
|
|
|
@@ -94,6 +94,38 @@ For the standard ones (ESA, DA, MQM), we expect each item to be a dictionary (co
|
|
|
94
94
|
... # definition of another item (document)
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
## Pre-filled Error Spans (ESA<sup>AI</sup> Support)
|
|
98
|
+
|
|
99
|
+
For workflows where you want to provide pre-filled error annotations (e.g., ESA<sup>AI</sup>), you can include an `error_spans` key in each item.
|
|
100
|
+
These spans will be loaded into the interface as existing annotations that users can review, modify, or delete.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
{
|
|
104
|
+
"src": "The quick brown fox jumps over the lazy dog.",
|
|
105
|
+
"tgt": "Rychlá hnědá liška skáče přes líného psa.",
|
|
106
|
+
"error_spans": [
|
|
107
|
+
{
|
|
108
|
+
"start_i": 0, # character index start (inclusive)
|
|
109
|
+
"end_i": 5, # character index end (inclusive)
|
|
110
|
+
"severity": "minor", # "minor", "major", "neutral", or null
|
|
111
|
+
"category": null # MQM category string or null
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"start_i": 27,
|
|
115
|
+
"end_i": 32,
|
|
116
|
+
"severity": "major",
|
|
117
|
+
"category": null
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For **listwise** template, `error_spans` is a 2D array where each inner array corresponds to error spans for that candidate.
|
|
124
|
+
|
|
125
|
+
See [examples/esaai_prefilled.json](examples/esaai_prefilled.json) for a complete example.
|
|
126
|
+
|
|
127
|
+
## Single-stream Assignment
|
|
128
|
+
|
|
97
129
|
We also support a simple allocation where all annotators draw from the same pool (`single-stream`). Items are randomly assigned to annotators from the pool of unfinished items:
|
|
98
130
|
```python
|
|
99
131
|
{
|
|
@@ -117,7 +149,7 @@ We also support dynamic allocation of annotations (`dynamic`, not yet ⚠️), w
|
|
|
117
149
|
"campaign_id": "my campaign 6",
|
|
118
150
|
"info": {
|
|
119
151
|
"assignment": "dynamic",
|
|
120
|
-
"template": "
|
|
152
|
+
"template": "listwise",
|
|
121
153
|
"protocol_k": 5,
|
|
122
154
|
"num_users": 50,
|
|
123
155
|
},
|
|
@@ -133,6 +165,25 @@ pearmut add my_campaign_4.json
|
|
|
133
165
|
pearmut run
|
|
134
166
|
```
|
|
135
167
|
|
|
168
|
+
## Campaign options
|
|
169
|
+
|
|
170
|
+
In summary, you can select from the assignment types
|
|
171
|
+
|
|
172
|
+
- `task-based`: each user has a predefined set of items
|
|
173
|
+
- `single-stream`: all users are annotating together the same set of items
|
|
174
|
+
- `dynamic`: WIP ⚠️
|
|
175
|
+
|
|
176
|
+
and independently of that select your protocol template:
|
|
177
|
+
|
|
178
|
+
- `pointwise`: evaluate a single output given a single output
|
|
179
|
+
- `protocol_score`: ask for score 0 to 100
|
|
180
|
+
- `protocol_error_spans`: ask for highlighting error spans
|
|
181
|
+
- `protocol_error_categories`: ask for highlighting error categories
|
|
182
|
+
- `listwise`: evaluate multiple outputs at the same time given a single output ⚠️
|
|
183
|
+
- `protocol_score`: ask for score 0 to 100
|
|
184
|
+
- `protocol_error_spans`: ask for highlighting error spans
|
|
185
|
+
- `protocol_error_categories`: ask for highlighting error categories
|
|
186
|
+
|
|
136
187
|
## Campaign management
|
|
137
188
|
|
|
138
189
|
When adding new campaigns or launching pearmut, a management link is shown that gives an overview of annotator progress but also an easy access to the annotation links or resetting the task progress (no data will be lost).
|
|
@@ -149,7 +200,7 @@ An intentionally incorrect token can be shown if the annotations don't pass qual
|
|
|
149
200
|
|
|
150
201
|
We also support anything HTML-compatible both on the input and on the output.
|
|
151
202
|
This includes embedded YouTube videos, or even simple `<video ` tags that point to some resource somewhere.
|
|
152
|
-
For an example, try [examples/
|
|
203
|
+
For an example, try [examples/multimodal.json](examples/multimodal.json).
|
|
153
204
|
Tip: make sure the elements are already appropriately styled.
|
|
154
205
|
|
|
155
206
|
<img width="800" alt="Preview of multimodal elements in Pearmut" src="https://github.com/user-attachments/assets/f34a1a3e-ad95-4114-95ee-8a49e8003faf" />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pearmut
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A tool for evaluation of model outputs, primarily MT.
|
|
5
5
|
Author-email: Vilém Zouhar <vilem.zouhar@gmail.com>
|
|
6
6
|
License: apache-2.0
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
Pearmut is a **Platform for Evaluation and Reviewing of Multilingual Tasks**.
|
|
25
25
|
It evaluates model outputs, primarily translation but also various other NLP tasks.
|
|
26
|
-
Supports multimodality (text, video, audio, images) and a variety of annotation protocols (DA, ESA, MQM, paired ESA, etc).
|
|
26
|
+
Supports multimodality (text, video, audio, images) and a variety of annotation protocols ([DA](https://aclanthology.org/N15-1124/), [ESA](https://aclanthology.org/2024.wmt-1.131/), [ESA<sup>AI</sup>](https://aclanthology.org/2025.naacl-long.255/), [MQM](https://doi.org/10.1162/tacl_a_00437), paired ESA, etc).
|
|
27
27
|
|
|
28
28
|
[](https://pypi.org/project/pearmut)
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ Supports multimodality (text, video, audio, images) and a variety of annotation
|
|
|
31
31
|
|
|
32
32
|
[](https://pypi.org/project/pearmut/)
|
|
33
33
|
|
|
34
|
-
[](https://github.com/zouharvi/pearmut/actions/workflows/test.yml)
|
|
35
35
|
|
|
36
36
|
<img width="1000" alt="Screenshot of ESA/MQM interface" src="https://github.com/user-attachments/assets/f14c91a5-44d7-4248-ada9-387e95ca59d0" />
|
|
37
37
|
|
|
@@ -115,6 +115,38 @@ For the standard ones (ESA, DA, MQM), we expect each item to be a dictionary (co
|
|
|
115
115
|
... # definition of another item (document)
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
## Pre-filled Error Spans (ESA<sup>AI</sup> Support)
|
|
119
|
+
|
|
120
|
+
For workflows where you want to provide pre-filled error annotations (e.g., ESA<sup>AI</sup>), you can include an `error_spans` key in each item.
|
|
121
|
+
These spans will be loaded into the interface as existing annotations that users can review, modify, or delete.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
{
|
|
125
|
+
"src": "The quick brown fox jumps over the lazy dog.",
|
|
126
|
+
"tgt": "Rychlá hnědá liška skáče přes líného psa.",
|
|
127
|
+
"error_spans": [
|
|
128
|
+
{
|
|
129
|
+
"start_i": 0, # character index start (inclusive)
|
|
130
|
+
"end_i": 5, # character index end (inclusive)
|
|
131
|
+
"severity": "minor", # "minor", "major", "neutral", or null
|
|
132
|
+
"category": null # MQM category string or null
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"start_i": 27,
|
|
136
|
+
"end_i": 32,
|
|
137
|
+
"severity": "major",
|
|
138
|
+
"category": null
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
For **listwise** template, `error_spans` is a 2D array where each inner array corresponds to error spans for that candidate.
|
|
145
|
+
|
|
146
|
+
See [examples/esaai_prefilled.json](examples/esaai_prefilled.json) for a complete example.
|
|
147
|
+
|
|
148
|
+
## Single-stream Assignment
|
|
149
|
+
|
|
118
150
|
We also support a simple allocation where all annotators draw from the same pool (`single-stream`). Items are randomly assigned to annotators from the pool of unfinished items:
|
|
119
151
|
```python
|
|
120
152
|
{
|
|
@@ -138,7 +170,7 @@ We also support dynamic allocation of annotations (`dynamic`, not yet ⚠️), w
|
|
|
138
170
|
"campaign_id": "my campaign 6",
|
|
139
171
|
"info": {
|
|
140
172
|
"assignment": "dynamic",
|
|
141
|
-
"template": "
|
|
173
|
+
"template": "listwise",
|
|
142
174
|
"protocol_k": 5,
|
|
143
175
|
"num_users": 50,
|
|
144
176
|
},
|
|
@@ -154,6 +186,25 @@ pearmut add my_campaign_4.json
|
|
|
154
186
|
pearmut run
|
|
155
187
|
```
|
|
156
188
|
|
|
189
|
+
## Campaign options
|
|
190
|
+
|
|
191
|
+
In summary, you can select from the assignment types
|
|
192
|
+
|
|
193
|
+
- `task-based`: each user has a predefined set of items
|
|
194
|
+
- `single-stream`: all users are annotating together the same set of items
|
|
195
|
+
- `dynamic`: WIP ⚠️
|
|
196
|
+
|
|
197
|
+
and independently of that select your protocol template:
|
|
198
|
+
|
|
199
|
+
- `pointwise`: evaluate a single output given a single output
|
|
200
|
+
- `protocol_score`: ask for score 0 to 100
|
|
201
|
+
- `protocol_error_spans`: ask for highlighting error spans
|
|
202
|
+
- `protocol_error_categories`: ask for highlighting error categories
|
|
203
|
+
- `listwise`: evaluate multiple outputs at the same time given a single output ⚠️
|
|
204
|
+
- `protocol_score`: ask for score 0 to 100
|
|
205
|
+
- `protocol_error_spans`: ask for highlighting error spans
|
|
206
|
+
- `protocol_error_categories`: ask for highlighting error categories
|
|
207
|
+
|
|
157
208
|
## Campaign management
|
|
158
209
|
|
|
159
210
|
When adding new campaigns or launching pearmut, a management link is shown that gives an overview of annotator progress but also an easy access to the annotation links or resetting the task progress (no data will be lost).
|
|
@@ -170,7 +221,7 @@ An intentionally incorrect token can be shown if the annotations don't pass qual
|
|
|
170
221
|
|
|
171
222
|
We also support anything HTML-compatible both on the input and on the output.
|
|
172
223
|
This includes embedded YouTube videos, or even simple `<video ` tags that point to some resource somewhere.
|
|
173
|
-
For an example, try [examples/
|
|
224
|
+
For an example, try [examples/multimodal.json](examples/multimodal.json).
|
|
174
225
|
Tip: make sure the elements are already appropriately styled.
|
|
175
226
|
|
|
176
227
|
<img width="800" alt="Preview of multimodal elements in Pearmut" src="https://github.com/user-attachments/assets/f34a1a3e-ad95-4114-95ee-8a49e8003faf" />
|
|
@@ -8,8 +8,8 @@ from fastapi.responses import JSONResponse
|
|
|
8
8
|
from fastapi.staticfiles import StaticFiles
|
|
9
9
|
from pydantic import BaseModel
|
|
10
10
|
|
|
11
|
-
from .assignment import get_next_item, reset_task, update_progress
|
|
12
|
-
from .utils import ROOT, load_progress_data, save_progress_data
|
|
11
|
+
from .assignment import get_i_item, get_next_item, reset_task, update_progress
|
|
12
|
+
from .utils import ROOT, load_progress_data, save_db_payload, save_progress_data
|
|
13
13
|
|
|
14
14
|
os.makedirs(f"{ROOT}/data/outputs", exist_ok=True)
|
|
15
15
|
|
|
@@ -36,7 +36,7 @@ class LogResponseRequest(BaseModel):
|
|
|
36
36
|
campaign_id: str
|
|
37
37
|
user_id: str
|
|
38
38
|
item_i: int
|
|
39
|
-
payload: Any
|
|
39
|
+
payload: dict[str, Any]
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
@app.post("/log-response")
|
|
@@ -45,6 +45,7 @@ async def _log_response(request: LogResponseRequest):
|
|
|
45
45
|
|
|
46
46
|
campaign_id = request.campaign_id
|
|
47
47
|
user_id = request.user_id
|
|
48
|
+
item_i = request.item_i
|
|
48
49
|
|
|
49
50
|
if campaign_id not in progress_data:
|
|
50
51
|
return JSONResponse(content={"error": "Unknown campaign ID"}, status_code=400)
|
|
@@ -52,8 +53,7 @@ async def _log_response(request: LogResponseRequest):
|
|
|
52
53
|
return JSONResponse(content={"error": "Unknown user ID"}, status_code=400)
|
|
53
54
|
|
|
54
55
|
# append response to the output log
|
|
55
|
-
|
|
56
|
-
log_file.write(json.dumps(request.payload, ensure_ascii=False) + "\n")
|
|
56
|
+
save_db_payload(campaign_id, request.payload | {"user_id": user_id, "item_i": item_i})
|
|
57
57
|
|
|
58
58
|
# if actions were submitted, we can log time data
|
|
59
59
|
if "actions" in request.payload:
|
|
@@ -97,6 +97,32 @@ async def _get_next_item(request: NextItemRequest):
|
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
class GetItemRequest(BaseModel):
|
|
101
|
+
campaign_id: str
|
|
102
|
+
user_id: str
|
|
103
|
+
item_i: int
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.post("/get-i-item")
|
|
107
|
+
async def _get_i_item(request: GetItemRequest):
|
|
108
|
+
campaign_id = request.campaign_id
|
|
109
|
+
user_id = request.user_id
|
|
110
|
+
item_i = request.item_i
|
|
111
|
+
|
|
112
|
+
if campaign_id not in progress_data:
|
|
113
|
+
return JSONResponse(content={"error": "Unknown campaign ID"}, status_code=400)
|
|
114
|
+
if user_id not in progress_data[campaign_id]:
|
|
115
|
+
return JSONResponse(content={"error": "Unknown user ID"}, status_code=400)
|
|
116
|
+
|
|
117
|
+
return get_i_item(
|
|
118
|
+
campaign_id,
|
|
119
|
+
user_id,
|
|
120
|
+
tasks_data,
|
|
121
|
+
progress_data,
|
|
122
|
+
item_i,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
100
126
|
class DashboardDataRequest(BaseModel):
|
|
101
127
|
campaign_id: str
|
|
102
128
|
token: str | None = None
|
|
@@ -3,6 +3,8 @@ from typing import Any
|
|
|
3
3
|
|
|
4
4
|
from fastapi.responses import JSONResponse
|
|
5
5
|
|
|
6
|
+
from .utils import get_db_log_item
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
def _completed_response(
|
|
8
10
|
progress_data: dict,
|
|
@@ -37,13 +39,121 @@ def get_next_item(
|
|
|
37
39
|
if assignment == "task-based":
|
|
38
40
|
return get_next_item_taskbased(campaign_id, user_id, tasks_data, progress_data)
|
|
39
41
|
elif assignment == "single-stream":
|
|
40
|
-
return
|
|
42
|
+
return get_next_item_singlestream(campaign_id, user_id, tasks_data, progress_data)
|
|
41
43
|
elif assignment == "dynamic":
|
|
42
44
|
return get_next_item_dynamic(campaign_id, user_id, tasks_data, progress_data)
|
|
43
45
|
else:
|
|
44
46
|
return JSONResponse(content={"error": "Unknown campaign assignment type"}, status_code=400)
|
|
45
47
|
|
|
46
48
|
|
|
49
|
+
def get_i_item(
|
|
50
|
+
campaign_id: str,
|
|
51
|
+
user_id: str,
|
|
52
|
+
tasks_data: dict,
|
|
53
|
+
progress_data: dict,
|
|
54
|
+
item_i: int,
|
|
55
|
+
) -> JSONResponse:
|
|
56
|
+
"""
|
|
57
|
+
Get a specific item by index for the user in the specified campaign.
|
|
58
|
+
"""
|
|
59
|
+
assignment = tasks_data[campaign_id]["info"]["assignment"]
|
|
60
|
+
if assignment == "task-based":
|
|
61
|
+
return get_i_item_taskbased(campaign_id, user_id, tasks_data, progress_data, item_i)
|
|
62
|
+
elif assignment == "single-stream":
|
|
63
|
+
return get_i_item_singlestream(campaign_id, user_id, tasks_data, progress_data, item_i)
|
|
64
|
+
else:
|
|
65
|
+
return JSONResponse(content={"error": "Get item not supported for this assignment type"}, status_code=400)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_i_item_taskbased(
|
|
69
|
+
campaign_id: str,
|
|
70
|
+
user_id: str,
|
|
71
|
+
data_all: dict,
|
|
72
|
+
progress_data: dict,
|
|
73
|
+
item_i: int,
|
|
74
|
+
) -> JSONResponse:
|
|
75
|
+
"""
|
|
76
|
+
Get specific item for task-based protocol.
|
|
77
|
+
"""
|
|
78
|
+
user_progress = progress_data[campaign_id][user_id]
|
|
79
|
+
if all(user_progress["progress"]):
|
|
80
|
+
return _completed_response(progress_data, campaign_id, user_id)
|
|
81
|
+
|
|
82
|
+
# try to get existing annotations if any
|
|
83
|
+
items_existing = get_db_log_item(campaign_id, user_id, item_i)
|
|
84
|
+
if items_existing:
|
|
85
|
+
# get the latest ones
|
|
86
|
+
payload_existing = items_existing[-1]["annotations"]
|
|
87
|
+
|
|
88
|
+
if item_i < 0 or item_i >= len(data_all[campaign_id]["data"][user_id]):
|
|
89
|
+
return JSONResponse(
|
|
90
|
+
content={"status": "error", "message": "Item index out of range"},
|
|
91
|
+
status_code=400
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return JSONResponse(
|
|
95
|
+
content={
|
|
96
|
+
"status": "ok",
|
|
97
|
+
"progress": user_progress["progress"],
|
|
98
|
+
"time": user_progress["time"],
|
|
99
|
+
"info": {
|
|
100
|
+
"item_i": item_i,
|
|
101
|
+
} | {
|
|
102
|
+
k: v
|
|
103
|
+
for k, v in data_all[campaign_id]["info"].items()
|
|
104
|
+
if k.startswith("protocol")
|
|
105
|
+
},
|
|
106
|
+
"payload": data_all[campaign_id]["data"][user_id][item_i]
|
|
107
|
+
} | ({"payload_existing": payload_existing} if items_existing else {}),
|
|
108
|
+
status_code=200
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_i_item_singlestream(
|
|
113
|
+
campaign_id: str,
|
|
114
|
+
user_id: str,
|
|
115
|
+
data_all: dict,
|
|
116
|
+
progress_data: dict,
|
|
117
|
+
item_i: int,
|
|
118
|
+
) -> JSONResponse:
|
|
119
|
+
"""
|
|
120
|
+
Get specific item for single-stream assignment.
|
|
121
|
+
"""
|
|
122
|
+
user_progress = progress_data[campaign_id][user_id]
|
|
123
|
+
if all(user_progress["progress"]):
|
|
124
|
+
return _completed_response(progress_data, campaign_id, user_id)
|
|
125
|
+
|
|
126
|
+
# try to get existing annotations if any
|
|
127
|
+
# note the None user_id since it is shared
|
|
128
|
+
items_existing = get_db_log_item(campaign_id, None, item_i)
|
|
129
|
+
if items_existing:
|
|
130
|
+
# get the latest ones
|
|
131
|
+
payload_existing = items_existing[-1]["annotations"]
|
|
132
|
+
|
|
133
|
+
if item_i < 0 or item_i >= len(data_all[campaign_id]["data"]):
|
|
134
|
+
return JSONResponse(
|
|
135
|
+
content={"status": "error", "message": "Item index out of range"},
|
|
136
|
+
status_code=400
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return JSONResponse(
|
|
140
|
+
content={
|
|
141
|
+
"status": "ok",
|
|
142
|
+
"progress": user_progress["progress"],
|
|
143
|
+
"time": user_progress["time"],
|
|
144
|
+
"info": {
|
|
145
|
+
"item_i": item_i,
|
|
146
|
+
} | {
|
|
147
|
+
k: v
|
|
148
|
+
for k, v in data_all[campaign_id]["info"].items()
|
|
149
|
+
if k.startswith("protocol")
|
|
150
|
+
},
|
|
151
|
+
"payload": data_all[campaign_id]["data"][item_i]
|
|
152
|
+
} | ({"payload_existing": payload_existing} if items_existing else {}),
|
|
153
|
+
status_code=200
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
47
157
|
def get_next_item_taskbased(
|
|
48
158
|
campaign_id: str,
|
|
49
159
|
user_id: str,
|
|
@@ -51,7 +161,7 @@ def get_next_item_taskbased(
|
|
|
51
161
|
progress_data: dict,
|
|
52
162
|
) -> JSONResponse:
|
|
53
163
|
"""
|
|
54
|
-
Get the next item for task-based
|
|
164
|
+
Get the next item for task-based assignment.
|
|
55
165
|
"""
|
|
56
166
|
user_progress = progress_data[campaign_id][user_id]
|
|
57
167
|
if all(user_progress["progress"]):
|
|
@@ -59,6 +169,13 @@ def get_next_item_taskbased(
|
|
|
59
169
|
|
|
60
170
|
# find first incomplete item
|
|
61
171
|
item_i = min([i for i, v in enumerate(user_progress["progress"]) if not v])
|
|
172
|
+
|
|
173
|
+
# try to get existing annotations if any
|
|
174
|
+
items_existing = get_db_log_item(campaign_id, user_id, item_i)
|
|
175
|
+
if items_existing:
|
|
176
|
+
# get the latest ones
|
|
177
|
+
payload_existing = items_existing[-1]["annotations"]
|
|
178
|
+
|
|
62
179
|
return JSONResponse(
|
|
63
180
|
content={
|
|
64
181
|
"status": "ok",
|
|
@@ -71,23 +188,20 @@ def get_next_item_taskbased(
|
|
|
71
188
|
for k, v in data_all[campaign_id]["info"].items()
|
|
72
189
|
if k.startswith("protocol")
|
|
73
190
|
},
|
|
74
|
-
"payload": data_all[campaign_id]["data"][user_id][item_i]
|
|
191
|
+
"payload": data_all[campaign_id]["data"][user_id][item_i]
|
|
192
|
+
} | ({"payload_existing": payload_existing} if items_existing else {}),
|
|
75
193
|
status_code=200
|
|
76
194
|
)
|
|
77
195
|
|
|
78
196
|
|
|
79
|
-
def
|
|
80
|
-
raise NotImplementedError("Dynamic protocol is not implemented yet.")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def get_next_item_single_stream(
|
|
197
|
+
def get_next_item_singlestream(
|
|
84
198
|
campaign_id: str,
|
|
85
199
|
user_id: str,
|
|
86
200
|
data_all: dict,
|
|
87
201
|
progress_data: dict,
|
|
88
202
|
) -> JSONResponse:
|
|
89
203
|
"""
|
|
90
|
-
Get the next item for single-stream
|
|
204
|
+
Get the next item for single-stream assignment.
|
|
91
205
|
In this mode, all users share the same pool of items.
|
|
92
206
|
Items are randomly selected from unfinished items.
|
|
93
207
|
|
|
@@ -104,6 +218,13 @@ def get_next_item_single_stream(
|
|
|
104
218
|
incomplete_indices = [i for i, v in enumerate(progress) if not v]
|
|
105
219
|
item_i = random.choice(incomplete_indices)
|
|
106
220
|
|
|
221
|
+
# try to get existing annotations if any
|
|
222
|
+
# note the None user_id since it is shared
|
|
223
|
+
items_existing = get_db_log_item(campaign_id, None, item_i)
|
|
224
|
+
if items_existing:
|
|
225
|
+
# get the latest ones
|
|
226
|
+
payload_existing = items_existing[-1]["annotations"]
|
|
227
|
+
|
|
107
228
|
return JSONResponse(
|
|
108
229
|
content={
|
|
109
230
|
"status": "ok",
|
|
@@ -116,11 +237,18 @@ def get_next_item_single_stream(
|
|
|
116
237
|
for k, v in data_all[campaign_id]["info"].items()
|
|
117
238
|
if k.startswith("protocol")
|
|
118
239
|
},
|
|
119
|
-
"payload": data_all[campaign_id]["data"][item_i]
|
|
240
|
+
"payload": data_all[campaign_id]["data"][item_i]
|
|
241
|
+
} | ({"payload_existing": payload_existing} if items_existing else {}),
|
|
120
242
|
status_code=200
|
|
121
243
|
)
|
|
122
244
|
|
|
123
245
|
|
|
246
|
+
|
|
247
|
+
def get_next_item_dynamic(campaign_data: dict, user_id: str, progress_data: dict, data_all: dict):
|
|
248
|
+
raise NotImplementedError("Dynamic protocol is not implemented yet.")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
124
252
|
def _reset_user_time(progress_data: dict, campaign_id: str, user_id: str) -> None:
|
|
125
253
|
"""Reset time tracking fields for a user."""
|
|
126
254
|
progress_data[campaign_id][user_id]["time"] = 0.0
|