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.
Files changed (26) hide show
  1. {pearmut-0.1.2 → pearmut-0.1.3}/PKG-INFO +56 -5
  2. {pearmut-0.1.2 → pearmut-0.1.3}/README.md +55 -4
  3. {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/PKG-INFO +56 -5
  4. {pearmut-0.1.2 → pearmut-0.1.3}/pyproject.toml +1 -1
  5. {pearmut-0.1.2 → pearmut-0.1.3}/server/app.py +31 -5
  6. {pearmut-0.1.2 → pearmut-0.1.3}/server/assignment.py +138 -10
  7. {pearmut-0.1.2 → pearmut-0.1.3}/server/static/listwise.bundle.js +1 -1
  8. {pearmut-0.1.2 → pearmut-0.1.3}/server/static/listwise.html +1 -1
  9. pearmut-0.1.3/server/static/pointwise.bundle.js +1 -0
  10. pearmut-0.1.3/server/utils.py +101 -0
  11. pearmut-0.1.2/server/static/pointwise.bundle.js +0 -1
  12. pearmut-0.1.2/server/utils.py +0 -48
  13. {pearmut-0.1.2 → pearmut-0.1.3}/LICENSE +0 -0
  14. {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/SOURCES.txt +0 -0
  15. {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/dependency_links.txt +0 -0
  16. {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/entry_points.txt +0 -0
  17. {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/requires.txt +0 -0
  18. {pearmut-0.1.2 → pearmut-0.1.3}/pearmut.egg-info/top_level.txt +0 -0
  19. {pearmut-0.1.2 → pearmut-0.1.3}/server/cli.py +0 -0
  20. {pearmut-0.1.2 → pearmut-0.1.3}/server/static/assets/favicon.svg +0 -0
  21. {pearmut-0.1.2 → pearmut-0.1.3}/server/static/assets/style.css +0 -0
  22. {pearmut-0.1.2 → pearmut-0.1.3}/server/static/dashboard.bundle.js +0 -0
  23. {pearmut-0.1.2 → pearmut-0.1.3}/server/static/dashboard.html +0 -0
  24. {pearmut-0.1.2 → pearmut-0.1.3}/server/static/index.html +0 -0
  25. {pearmut-0.1.2 → pearmut-0.1.3}/server/static/pointwise.html +0 -0
  26. {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.2
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
  [![PyPi version](https://badgen.net/pypi/v/pearmut/)](https://pypi.org/project/pearmut)
29
29
  &nbsp;
@@ -31,7 +31,7 @@ Supports multimodality (text, video, audio, images) and a variety of annotation
31
31
  &nbsp;
32
32
  [![PyPi license](https://badgen.net/pypi/license/pearmut/)](https://pypi.org/project/pearmut/)
33
33
  &nbsp;
34
- [![build status](https://github.com/zouharvi/pearmut/actions/workflows/ci.yml/badge.svg)](https://github.com/zouharvi/pearmut/actions/workflows/ci.yml)
34
+ [![build status](https://github.com/zouharvi/pearmut/actions/workflows/test.yml/badge.svg)](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": "kway",
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/mock_multimodal.json](examples/mock_multimodal.json).
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
  [![PyPi version](https://badgen.net/pypi/v/pearmut/)](https://pypi.org/project/pearmut)
8
8
  &nbsp;
@@ -10,7 +10,7 @@ Supports multimodality (text, video, audio, images) and a variety of annotation
10
10
  &nbsp;
11
11
  [![PyPi license](https://badgen.net/pypi/license/pearmut/)](https://pypi.org/project/pearmut/)
12
12
  &nbsp;
13
- [![build status](https://github.com/zouharvi/pearmut/actions/workflows/ci.yml/badge.svg)](https://github.com/zouharvi/pearmut/actions/workflows/ci.yml)
13
+ [![build status](https://github.com/zouharvi/pearmut/actions/workflows/test.yml/badge.svg)](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": "kway",
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/mock_multimodal.json](examples/mock_multimodal.json).
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.2
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
  [![PyPi version](https://badgen.net/pypi/v/pearmut/)](https://pypi.org/project/pearmut)
29
29
  &nbsp;
@@ -31,7 +31,7 @@ Supports multimodality (text, video, audio, images) and a variety of annotation
31
31
  &nbsp;
32
32
  [![PyPi license](https://badgen.net/pypi/license/pearmut/)](https://pypi.org/project/pearmut/)
33
33
  &nbsp;
34
- [![build status](https://github.com/zouharvi/pearmut/actions/workflows/ci.yml/badge.svg)](https://github.com/zouharvi/pearmut/actions/workflows/ci.yml)
34
+ [![build status](https://github.com/zouharvi/pearmut/actions/workflows/test.yml/badge.svg)](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": "kway",
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/mock_multimodal.json](examples/mock_multimodal.json).
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" />
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pearmut"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "A tool for evaluation of model outputs, primarily MT."
5
5
  readme = "README.md"
6
6
  license = { text = "apache-2.0" }
@@ -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
- with open(f"{ROOT}/data/outputs/{campaign_id}.jsonl", "a") as log_file:
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 get_next_item_single_stream(campaign_id, user_id, tasks_data, progress_data)
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 protocol.
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 get_next_item_dynamic(campaign_data: dict, user_id: str, progress_data: dict, data_all: dict):
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 protocol.
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