pearmut 0.2.9__tar.gz → 0.2.11__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 (28) hide show
  1. {pearmut-0.2.9 → pearmut-0.2.11}/PKG-INFO +53 -3
  2. {pearmut-0.2.9 → pearmut-0.2.11}/README.md +52 -2
  3. {pearmut-0.2.9 → pearmut-0.2.11}/pearmut.egg-info/PKG-INFO +53 -3
  4. {pearmut-0.2.9 → pearmut-0.2.11}/pearmut.egg-info/SOURCES.txt +2 -2
  5. {pearmut-0.2.9 → pearmut-0.2.11}/pyproject.toml +1 -1
  6. {pearmut-0.2.9 → pearmut-0.2.11}/server/app.py +20 -2
  7. {pearmut-0.2.9 → pearmut-0.2.11}/server/assignment.py +24 -8
  8. {pearmut-0.2.9 → pearmut-0.2.11}/server/cli.py +3 -8
  9. pearmut-0.2.11/server/static/dashboard.bundle.js +1 -0
  10. {pearmut-0.2.9 → pearmut-0.2.11}/server/static/dashboard.html +1 -1
  11. pearmut-0.2.11/server/static/index.html +1 -0
  12. pearmut-0.2.11/server/static/listwise.bundle.js +1 -0
  13. {pearmut-0.2.9 → pearmut-0.2.11}/server/static/listwise.html +2 -2
  14. pearmut-0.2.11/server/static/pointwise.bundle.js +1 -0
  15. {pearmut-0.2.9 → pearmut-0.2.11}/server/static/pointwise.html +2 -2
  16. pearmut-0.2.9/server/static/dashboard.bundle.js +0 -1
  17. pearmut-0.2.9/server/static/index.html +0 -1
  18. pearmut-0.2.9/server/static/listwise.bundle.js +0 -1
  19. pearmut-0.2.9/server/static/pointwise.bundle.js +0 -1
  20. {pearmut-0.2.9 → pearmut-0.2.11}/LICENSE +0 -0
  21. {pearmut-0.2.9 → pearmut-0.2.11}/pearmut.egg-info/dependency_links.txt +0 -0
  22. {pearmut-0.2.9 → pearmut-0.2.11}/pearmut.egg-info/entry_points.txt +0 -0
  23. {pearmut-0.2.9 → pearmut-0.2.11}/pearmut.egg-info/requires.txt +0 -0
  24. {pearmut-0.2.9 → pearmut-0.2.11}/pearmut.egg-info/top_level.txt +0 -0
  25. {pearmut-0.2.9/server/static/assets → pearmut-0.2.11/server/static}/favicon.svg +0 -0
  26. {pearmut-0.2.9/server/static/assets → pearmut-0.2.11/server/static}/style.css +0 -0
  27. {pearmut-0.2.9 → pearmut-0.2.11}/server/utils.py +0 -0
  28. {pearmut-0.2.9 → pearmut-0.2.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pearmut
3
- Version: 0.2.9
3
+ Version: 0.2.11
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: MIT
@@ -47,9 +47,13 @@ Dynamic: license-file
47
47
  - [Hosting Assets](#hosting-assets)
48
48
  - [Campaign Management](#campaign-management)
49
49
  - [CLI Commands](#cli-commands)
50
+ - [Terminology](#terminology)
50
51
  - [Development](#development)
51
52
  - [Citation](#citation)
52
53
 
54
+
55
+ **Error Span** — A highlighted segment of text marked as containing an error, with optional severity (`minor`, `major`, `neutral`) and MQM category labels.
56
+
53
57
  ## Quick Start
54
58
 
55
59
  Install and run locally without cloning:
@@ -193,7 +197,21 @@ Add `validation` rules for tutorials or attention checks:
193
197
  - **Silent attention checks**: Omit `warning` to log failures without notification (quality control)
194
198
 
195
199
  For listwise, `validation` is an array (one per candidate). Dashboard shows ✅/❌ based on `validation_threshold` in `info` (integer for max failed count, float \[0,1\) for max proportion, default 0).
196
- See [examples/tutorial_pointwise.json](examples/tutorial_pointwise.json) and [examples/tutorial_listwise.json](examples/tutorial_listwise.json).
200
+
201
+ **Listwise score comparison:** Use `score_greaterthan` to ensure one candidate scores higher than another:
202
+ ```python
203
+ {
204
+ "src": "AI transforms industries.",
205
+ "tgt": ["UI transformuje průmysly.", "Umělá inteligence mění obory."],
206
+ "validation": [
207
+ {"warning": "A has error, score 20-40.", "score": [20, 40]},
208
+ {"warning": "B is correct and must score higher than A.", "score": [70, 90], "score_greaterthan": 0}
209
+ ]
210
+ }
211
+ ```
212
+ The `score_greaterthan` field specifies the index of the candidate that must have a lower score than the current candidate.
213
+
214
+ See [examples/tutorial_pointwise.json](examples/tutorial_pointwise.json), [examples/tutorial_listwise.json](examples/tutorial_listwise.json), and [examples/tutorial_listwise_score_greaterthan.json](examples/tutorial_listwise_score_greaterthan.json).
197
215
 
198
216
  ### Single-stream Assignment
199
217
 
@@ -294,6 +312,38 @@ Items need `model` field (pointwise) or `models` field (listwise) and the `proto
294
312
  ```
295
313
  See an example in [Campaign Management](#campaign-management)
296
314
 
315
+
316
+ ## Terminology
317
+
318
+ - **Campaign**: An annotation project that contains configuration, data, and user assignments. Each campaign has a unique identifier and is defined in a JSON file.
319
+ - **Campaign File**: A JSON file that defines the campaign configuration, including the campaign ID, assignment type, protocol settings, and annotation data.
320
+ - **Campaign ID**: A unique identifier for a campaign (e.g., `"wmt25_#_en-cs_CZ"`). Used to reference and manage specific campaigns.
321
+ - **Task**: A unit of work assigned to a user. In task-based assignment, each task consists of a predefined set of items for a specific user.
322
+ - **Item** — A single annotation unit within a task. For translation evaluation, an item typically represents a document (source text and target translation). Items can contain text, images, audio, or video.
323
+ - **Document** — A collection of one or more segments (sentence pairs or text units) that are evaluated together as a single item.
324
+ - **User** / **Annotator**: A person who performs annotations in a campaign. Each user is identified by a unique user ID and accesses the campaign through a unique URL.
325
+ - **Attention Check** — A validation item with known correct answers used to ensure annotator quality. Can be:
326
+ - **Loud**: Shows warning message and forces retry on failure
327
+ - **Silent**: Logs failures without notifying the user (for quality control analysis)
328
+ - **Token** — A completion code shown to users when they finish their annotations. Tokens verify the completion and whether the user passed quality control checks:
329
+ - **Pass Token** (`token_pass`): Shown when user meets validation thresholds
330
+ - **Fail Token** (`token_fail`): Shown when user fails to meet validation requirements
331
+ - **Tutorial**: An instructional validation item that teaches users how to annotate. Includes `allow_skip: true` to let users skip if they have seen it before.
332
+ - **Validation**: Quality control rules attached to items that check if annotations match expected criteria (score ranges, error span locations, etc.). Used for tutorials and attention checks.
333
+ - **Model**: The system or model that generated the output being evaluated (e.g., `"GPT-4"`, `"Claude"`). Used for tracking and ranking model performance.
334
+ - **Dashboard**: The management interface that shows campaign progress, annotator statistics, access links, and allows downloading annotations. Accessed via a special management URL with token authentication.
335
+ - **Protocol**: The annotation scheme defining what data is collected:
336
+ - **Score**: Numeric quality rating (0-100)
337
+ - **Error Spans**: Text highlights marking errors
338
+ - **Error Categories**: MQM taxonomy labels for errors
339
+ - **Template**: The annotation interface type:
340
+ - **Pointwise**: Evaluate one output at a time
341
+ - **Listwise**: Compare multiple outputs simultaneously
342
+ - **Assignment**: The method for distributing items to users:
343
+ - **Task-based**: Each user has predefined items
344
+ - **Single-stream**: Users draw from a shared pool with random assignment
345
+ - **Dynamic**: Work in progress
346
+
297
347
  ## Development
298
348
 
299
349
  Server responds to data-only requests from frontend (no template coupling). Frontend served from pre-built `static/` on install.
@@ -333,7 +383,7 @@ If you use this work in your paper, please cite as following.
333
383
  author={Vilém Zouhar},
334
384
  title={Pearmut: Platform for Evaluating and Reviewing of Multilingual Tasks},
335
385
  url={https://github.com/zouharvi/pearmut/},
336
- year={2025},
386
+ year={2026},
337
387
  }
338
388
  ```
339
389
 
@@ -27,9 +27,13 @@
27
27
  - [Hosting Assets](#hosting-assets)
28
28
  - [Campaign Management](#campaign-management)
29
29
  - [CLI Commands](#cli-commands)
30
+ - [Terminology](#terminology)
30
31
  - [Development](#development)
31
32
  - [Citation](#citation)
32
33
 
34
+
35
+ **Error Span** — A highlighted segment of text marked as containing an error, with optional severity (`minor`, `major`, `neutral`) and MQM category labels.
36
+
33
37
  ## Quick Start
34
38
 
35
39
  Install and run locally without cloning:
@@ -173,7 +177,21 @@ Add `validation` rules for tutorials or attention checks:
173
177
  - **Silent attention checks**: Omit `warning` to log failures without notification (quality control)
174
178
 
175
179
  For listwise, `validation` is an array (one per candidate). Dashboard shows ✅/❌ based on `validation_threshold` in `info` (integer for max failed count, float \[0,1\) for max proportion, default 0).
176
- See [examples/tutorial_pointwise.json](examples/tutorial_pointwise.json) and [examples/tutorial_listwise.json](examples/tutorial_listwise.json).
180
+
181
+ **Listwise score comparison:** Use `score_greaterthan` to ensure one candidate scores higher than another:
182
+ ```python
183
+ {
184
+ "src": "AI transforms industries.",
185
+ "tgt": ["UI transformuje průmysly.", "Umělá inteligence mění obory."],
186
+ "validation": [
187
+ {"warning": "A has error, score 20-40.", "score": [20, 40]},
188
+ {"warning": "B is correct and must score higher than A.", "score": [70, 90], "score_greaterthan": 0}
189
+ ]
190
+ }
191
+ ```
192
+ The `score_greaterthan` field specifies the index of the candidate that must have a lower score than the current candidate.
193
+
194
+ See [examples/tutorial_pointwise.json](examples/tutorial_pointwise.json), [examples/tutorial_listwise.json](examples/tutorial_listwise.json), and [examples/tutorial_listwise_score_greaterthan.json](examples/tutorial_listwise_score_greaterthan.json).
177
195
 
178
196
  ### Single-stream Assignment
179
197
 
@@ -274,6 +292,38 @@ Items need `model` field (pointwise) or `models` field (listwise) and the `proto
274
292
  ```
275
293
  See an example in [Campaign Management](#campaign-management)
276
294
 
295
+
296
+ ## Terminology
297
+
298
+ - **Campaign**: An annotation project that contains configuration, data, and user assignments. Each campaign has a unique identifier and is defined in a JSON file.
299
+ - **Campaign File**: A JSON file that defines the campaign configuration, including the campaign ID, assignment type, protocol settings, and annotation data.
300
+ - **Campaign ID**: A unique identifier for a campaign (e.g., `"wmt25_#_en-cs_CZ"`). Used to reference and manage specific campaigns.
301
+ - **Task**: A unit of work assigned to a user. In task-based assignment, each task consists of a predefined set of items for a specific user.
302
+ - **Item** — A single annotation unit within a task. For translation evaluation, an item typically represents a document (source text and target translation). Items can contain text, images, audio, or video.
303
+ - **Document** — A collection of one or more segments (sentence pairs or text units) that are evaluated together as a single item.
304
+ - **User** / **Annotator**: A person who performs annotations in a campaign. Each user is identified by a unique user ID and accesses the campaign through a unique URL.
305
+ - **Attention Check** — A validation item with known correct answers used to ensure annotator quality. Can be:
306
+ - **Loud**: Shows warning message and forces retry on failure
307
+ - **Silent**: Logs failures without notifying the user (for quality control analysis)
308
+ - **Token** — A completion code shown to users when they finish their annotations. Tokens verify the completion and whether the user passed quality control checks:
309
+ - **Pass Token** (`token_pass`): Shown when user meets validation thresholds
310
+ - **Fail Token** (`token_fail`): Shown when user fails to meet validation requirements
311
+ - **Tutorial**: An instructional validation item that teaches users how to annotate. Includes `allow_skip: true` to let users skip if they have seen it before.
312
+ - **Validation**: Quality control rules attached to items that check if annotations match expected criteria (score ranges, error span locations, etc.). Used for tutorials and attention checks.
313
+ - **Model**: The system or model that generated the output being evaluated (e.g., `"GPT-4"`, `"Claude"`). Used for tracking and ranking model performance.
314
+ - **Dashboard**: The management interface that shows campaign progress, annotator statistics, access links, and allows downloading annotations. Accessed via a special management URL with token authentication.
315
+ - **Protocol**: The annotation scheme defining what data is collected:
316
+ - **Score**: Numeric quality rating (0-100)
317
+ - **Error Spans**: Text highlights marking errors
318
+ - **Error Categories**: MQM taxonomy labels for errors
319
+ - **Template**: The annotation interface type:
320
+ - **Pointwise**: Evaluate one output at a time
321
+ - **Listwise**: Compare multiple outputs simultaneously
322
+ - **Assignment**: The method for distributing items to users:
323
+ - **Task-based**: Each user has predefined items
324
+ - **Single-stream**: Users draw from a shared pool with random assignment
325
+ - **Dynamic**: Work in progress
326
+
277
327
  ## Development
278
328
 
279
329
  Server responds to data-only requests from frontend (no template coupling). Frontend served from pre-built `static/` on install.
@@ -313,7 +363,7 @@ If you use this work in your paper, please cite as following.
313
363
  author={Vilém Zouhar},
314
364
  title={Pearmut: Platform for Evaluating and Reviewing of Multilingual Tasks},
315
365
  url={https://github.com/zouharvi/pearmut/},
316
- year={2025},
366
+ year={2026},
317
367
  }
318
368
  ```
319
369
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pearmut
3
- Version: 0.2.9
3
+ Version: 0.2.11
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: MIT
@@ -47,9 +47,13 @@ Dynamic: license-file
47
47
  - [Hosting Assets](#hosting-assets)
48
48
  - [Campaign Management](#campaign-management)
49
49
  - [CLI Commands](#cli-commands)
50
+ - [Terminology](#terminology)
50
51
  - [Development](#development)
51
52
  - [Citation](#citation)
52
53
 
54
+
55
+ **Error Span** — A highlighted segment of text marked as containing an error, with optional severity (`minor`, `major`, `neutral`) and MQM category labels.
56
+
53
57
  ## Quick Start
54
58
 
55
59
  Install and run locally without cloning:
@@ -193,7 +197,21 @@ Add `validation` rules for tutorials or attention checks:
193
197
  - **Silent attention checks**: Omit `warning` to log failures without notification (quality control)
194
198
 
195
199
  For listwise, `validation` is an array (one per candidate). Dashboard shows ✅/❌ based on `validation_threshold` in `info` (integer for max failed count, float \[0,1\) for max proportion, default 0).
196
- See [examples/tutorial_pointwise.json](examples/tutorial_pointwise.json) and [examples/tutorial_listwise.json](examples/tutorial_listwise.json).
200
+
201
+ **Listwise score comparison:** Use `score_greaterthan` to ensure one candidate scores higher than another:
202
+ ```python
203
+ {
204
+ "src": "AI transforms industries.",
205
+ "tgt": ["UI transformuje průmysly.", "Umělá inteligence mění obory."],
206
+ "validation": [
207
+ {"warning": "A has error, score 20-40.", "score": [20, 40]},
208
+ {"warning": "B is correct and must score higher than A.", "score": [70, 90], "score_greaterthan": 0}
209
+ ]
210
+ }
211
+ ```
212
+ The `score_greaterthan` field specifies the index of the candidate that must have a lower score than the current candidate.
213
+
214
+ See [examples/tutorial_pointwise.json](examples/tutorial_pointwise.json), [examples/tutorial_listwise.json](examples/tutorial_listwise.json), and [examples/tutorial_listwise_score_greaterthan.json](examples/tutorial_listwise_score_greaterthan.json).
197
215
 
198
216
  ### Single-stream Assignment
199
217
 
@@ -294,6 +312,38 @@ Items need `model` field (pointwise) or `models` field (listwise) and the `proto
294
312
  ```
295
313
  See an example in [Campaign Management](#campaign-management)
296
314
 
315
+
316
+ ## Terminology
317
+
318
+ - **Campaign**: An annotation project that contains configuration, data, and user assignments. Each campaign has a unique identifier and is defined in a JSON file.
319
+ - **Campaign File**: A JSON file that defines the campaign configuration, including the campaign ID, assignment type, protocol settings, and annotation data.
320
+ - **Campaign ID**: A unique identifier for a campaign (e.g., `"wmt25_#_en-cs_CZ"`). Used to reference and manage specific campaigns.
321
+ - **Task**: A unit of work assigned to a user. In task-based assignment, each task consists of a predefined set of items for a specific user.
322
+ - **Item** — A single annotation unit within a task. For translation evaluation, an item typically represents a document (source text and target translation). Items can contain text, images, audio, or video.
323
+ - **Document** — A collection of one or more segments (sentence pairs or text units) that are evaluated together as a single item.
324
+ - **User** / **Annotator**: A person who performs annotations in a campaign. Each user is identified by a unique user ID and accesses the campaign through a unique URL.
325
+ - **Attention Check** — A validation item with known correct answers used to ensure annotator quality. Can be:
326
+ - **Loud**: Shows warning message and forces retry on failure
327
+ - **Silent**: Logs failures without notifying the user (for quality control analysis)
328
+ - **Token** — A completion code shown to users when they finish their annotations. Tokens verify the completion and whether the user passed quality control checks:
329
+ - **Pass Token** (`token_pass`): Shown when user meets validation thresholds
330
+ - **Fail Token** (`token_fail`): Shown when user fails to meet validation requirements
331
+ - **Tutorial**: An instructional validation item that teaches users how to annotate. Includes `allow_skip: true` to let users skip if they have seen it before.
332
+ - **Validation**: Quality control rules attached to items that check if annotations match expected criteria (score ranges, error span locations, etc.). Used for tutorials and attention checks.
333
+ - **Model**: The system or model that generated the output being evaluated (e.g., `"GPT-4"`, `"Claude"`). Used for tracking and ranking model performance.
334
+ - **Dashboard**: The management interface that shows campaign progress, annotator statistics, access links, and allows downloading annotations. Accessed via a special management URL with token authentication.
335
+ - **Protocol**: The annotation scheme defining what data is collected:
336
+ - **Score**: Numeric quality rating (0-100)
337
+ - **Error Spans**: Text highlights marking errors
338
+ - **Error Categories**: MQM taxonomy labels for errors
339
+ - **Template**: The annotation interface type:
340
+ - **Pointwise**: Evaluate one output at a time
341
+ - **Listwise**: Compare multiple outputs simultaneously
342
+ - **Assignment**: The method for distributing items to users:
343
+ - **Task-based**: Each user has predefined items
344
+ - **Single-stream**: Users draw from a shared pool with random assignment
345
+ - **Dynamic**: Work in progress
346
+
297
347
  ## Development
298
348
 
299
349
  Server responds to data-only requests from frontend (no template coupling). Frontend served from pre-built `static/` on install.
@@ -333,7 +383,7 @@ If you use this work in your paper, please cite as following.
333
383
  author={Vilém Zouhar},
334
384
  title={Pearmut: Platform for Evaluating and Reviewing of Multilingual Tasks},
335
385
  url={https://github.com/zouharvi/pearmut/},
336
- year={2025},
386
+ year={2026},
337
387
  }
338
388
  ```
339
389
 
@@ -13,10 +13,10 @@ server/cli.py
13
13
  server/utils.py
14
14
  server/static/dashboard.bundle.js
15
15
  server/static/dashboard.html
16
+ server/static/favicon.svg
16
17
  server/static/index.html
17
18
  server/static/listwise.bundle.js
18
19
  server/static/listwise.html
19
20
  server/static/pointwise.bundle.js
20
21
  server/static/pointwise.html
21
- server/static/assets/favicon.svg
22
- server/static/assets/style.css
22
+ server/static/style.css
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pearmut"
3
- version = "0.2.9"
3
+ version = "0.2.11"
4
4
  description = "A tool for evaluation of model outputs, primarily MT."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -291,7 +291,11 @@ async def _download_annotations(
291
291
  with open(output_path, "r") as f:
292
292
  output[campaign_id] = [json.loads(x) for x in f.readlines()]
293
293
 
294
- return JSONResponse(content=output, status_code=200)
294
+ return JSONResponse(
295
+ content=output,
296
+ status_code=200,
297
+ headers={"Content-Disposition": 'inline; filename="annotations.json"'}
298
+ )
295
299
 
296
300
 
297
301
  @app.get("/download-progress")
@@ -315,7 +319,11 @@ async def _download_progress(
315
319
 
316
320
  output[cid] = progress_data[cid]
317
321
 
318
- return JSONResponse(content=output, status_code=200)
322
+ return JSONResponse(
323
+ content=output,
324
+ status_code=200,
325
+ headers={"Content-Disposition": 'inline; filename="progress.json"'}
326
+ )
319
327
 
320
328
 
321
329
  static_dir = f"{os.path.dirname(os.path.abspath(__file__))}/static/"
@@ -324,6 +332,16 @@ if not os.path.exists(static_dir + "index.html"):
324
332
  "Static directory not found. Please build the frontend first."
325
333
  )
326
334
 
335
+ # Mount user assets from data/assets/
336
+ assets_dir = f"{ROOT}/data/assets"
337
+ os.makedirs(assets_dir, exist_ok=True)
338
+
339
+ app.mount(
340
+ "/assets",
341
+ StaticFiles(directory=assets_dir, follow_symlink=True),
342
+ name="assets",
343
+ )
344
+
327
345
  app.mount(
328
346
  "/",
329
347
  StaticFiles(directory=static_dir, html=True, follow_symlink=True),
@@ -84,9 +84,13 @@ def get_i_item_taskbased(
84
84
 
85
85
  # try to get existing annotations if any
86
86
  items_existing = get_db_log_item(campaign_id, user_id, item_i)
87
+ payload_existing = None
87
88
  if items_existing:
88
89
  # get the latest ones
89
- payload_existing = items_existing[-1]["annotations"]
90
+ latest_item = items_existing[-1]
91
+ payload_existing = {"annotations": latest_item["annotations"]}
92
+ if "comment" in latest_item:
93
+ payload_existing["comment"] = latest_item["comment"]
90
94
 
91
95
  if item_i < 0 or item_i >= len(data_all[campaign_id]["data"][user_id]):
92
96
  return JSONResponse(
@@ -107,7 +111,7 @@ def get_i_item_taskbased(
107
111
  if k.startswith("protocol")
108
112
  },
109
113
  "payload": data_all[campaign_id]["data"][user_id][item_i]
110
- } | ({"payload_existing": payload_existing} if items_existing else {}),
114
+ } | ({"payload_existing": payload_existing} if payload_existing else {}),
111
115
  status_code=200
112
116
  )
113
117
 
@@ -127,9 +131,13 @@ def get_i_item_singlestream(
127
131
  # try to get existing annotations if any
128
132
  # note the None user_id since it is shared
129
133
  items_existing = get_db_log_item(campaign_id, None, item_i)
134
+ payload_existing = None
130
135
  if items_existing:
131
136
  # get the latest ones
132
- payload_existing = items_existing[-1]["annotations"]
137
+ latest_item = items_existing[-1]
138
+ payload_existing = {"annotations": latest_item["annotations"]}
139
+ if "comment" in latest_item:
140
+ payload_existing["comment"] = latest_item["comment"]
133
141
 
134
142
  if item_i < 0 or item_i >= len(data_all[campaign_id]["data"]):
135
143
  return JSONResponse(
@@ -150,7 +158,7 @@ def get_i_item_singlestream(
150
158
  if k.startswith("protocol")
151
159
  },
152
160
  "payload": data_all[campaign_id]["data"][item_i]
153
- } | ({"payload_existing": payload_existing} if items_existing else {}),
161
+ } | ({"payload_existing": payload_existing} if payload_existing else {}),
154
162
  status_code=200
155
163
  )
156
164
 
@@ -173,9 +181,13 @@ def get_next_item_taskbased(
173
181
 
174
182
  # try to get existing annotations if any
175
183
  items_existing = get_db_log_item(campaign_id, user_id, item_i)
184
+ payload_existing = None
176
185
  if items_existing:
177
186
  # get the latest ones
178
- payload_existing = items_existing[-1]["annotations"]
187
+ latest_item = items_existing[-1]
188
+ payload_existing = {"annotations": latest_item["annotations"]}
189
+ if "comment" in latest_item:
190
+ payload_existing["comment"] = latest_item["comment"]
179
191
 
180
192
  return JSONResponse(
181
193
  content={
@@ -190,7 +202,7 @@ def get_next_item_taskbased(
190
202
  if k.startswith("protocol")
191
203
  },
192
204
  "payload": data_all[campaign_id]["data"][user_id][item_i]
193
- } | ({"payload_existing": payload_existing} if items_existing else {}),
205
+ } | ({"payload_existing": payload_existing} if payload_existing else {}),
194
206
  status_code=200
195
207
  )
196
208
 
@@ -222,9 +234,13 @@ def get_next_item_singlestream(
222
234
  # try to get existing annotations if any
223
235
  # note the None user_id since it is shared
224
236
  items_existing = get_db_log_item(campaign_id, None, item_i)
237
+ payload_existing = None
225
238
  if items_existing:
226
239
  # get the latest ones
227
- payload_existing = items_existing[-1]["annotations"]
240
+ latest_item = items_existing[-1]
241
+ payload_existing = {"annotations": latest_item["annotations"]}
242
+ if "comment" in latest_item:
243
+ payload_existing["comment"] = latest_item["comment"]
228
244
 
229
245
  return JSONResponse(
230
246
  content={
@@ -239,7 +255,7 @@ def get_next_item_singlestream(
239
255
  if k.startswith("protocol")
240
256
  },
241
257
  "payload": data_all[campaign_id]["data"][item_i]
242
- } | ({"payload_existing": payload_existing} if items_existing else {}),
258
+ } | ({"payload_existing": payload_existing} if payload_existing else {}),
243
259
  status_code=200
244
260
  )
245
261
 
@@ -272,15 +272,10 @@ def _add_single_campaign(data_file, overwrite, server):
272
272
 
273
273
  if not os.path.isdir(assets_real_path):
274
274
  raise ValueError(f"Assets source path '{assets_real_path}' must be an existing directory.")
275
-
276
- if not os.path.isdir(STATIC_DIR):
277
- raise ValueError(
278
- f"Static directory '{STATIC_DIR}' does not exist. "
279
- "Please build the frontend first."
280
- )
281
275
 
282
276
  # Symlink path is based on the destination, stripping the 'assets/' prefix
283
- symlink_path = f"{STATIC_DIR}/{assets_destination}".rstrip("/")
277
+ # User assets are now stored under data/assets/ instead of static/assets/
278
+ symlink_path = f"{ROOT}/data/{assets_destination}".rstrip("/")
284
279
 
285
280
  # Remove existing symlink if present and we are overriding the same campaign
286
281
  if os.path.lexists(symlink_path):
@@ -392,7 +387,7 @@ def main():
392
387
  campaign_data = json.load(f)
393
388
  destination = campaign_data.get("info", {}).get("assets", {}).get("destination")
394
389
  if destination:
395
- symlink_path = f"{STATIC_DIR}/{destination}".rstrip("/")
390
+ symlink_path = f"{ROOT}/data/{destination}".rstrip("/")
396
391
  if os.path.islink(symlink_path):
397
392
  os.remove(symlink_path)
398
393
  print(f"Assets symlink removed: {symlink_path}")