# 🍐Pearmut
[](https://pypi.org/project/pearmut) [](https://pypi.python.org/pypi/pearmut/) [](https://pypi.org/project/pearmut/) [](https://github.com/zouharvi/pearmut/actions/workflows/test.yml) [](https://arxiv.org/abs/2601.02933) [](https://www.youtube.com/watch?v=rmUF3_WtF1M)
**Platform for Evaluation and Reviewing of Multilingual Tasks**: Evaluate model outputs for translation and NLP tasks with support for multimodal data (text, video, audio, images) and multiple annotation protocols ([DA](https://aclanthology.org/N15-1124/), [ESA](https://aclanthology.org/2024.wmt-1.131/), [ESAAI](https://aclanthology.org/2025.naacl-long.255/), [MQM](https://doi.org/10.1162/tacl_a_00437), [XSTS](https://arxiv.org/abs/2205.08533)/[XSTS+R+P](https://arxiv.org/abs/2603.16309), [cESA](https://arxiv.org/abs/2607.26640), and more!).
## Table of Contents
- [Quick Start](#quick-start)
- [Campaign Configuration](#campaign-configuration)
- [Basic Structure](#basic-structure)
- [Assignment Types](#assignment-types)
- [Advanced Features](#advanced-features)
- [Pre-filled Error Spans (ESAAI)](#pre-filled-error-spans-esaai)
- [Custom MQM Taxonomy](#custom-mqm-taxonomy)
- [Custom Score Sliders](#custom-score-sliders)
- [Textfield comment box](#textfield-comment-box)
- [Tutorial and Attention Checks](#tutorial-and-attention-checks)
- [Form Items for User Metadata](#form-items-for-user-metadata)
- [Pre-defined User IDs and Tokens](#pre-defined-user-ids-and-tokens)
- [Multimodal Annotations](#multimodal-annotations)
- [Hosting Assets](#hosting-assets)
- [Campaign Management](#campaign-management)
- [Custom Completion Messages](#custom-completion-messages)
- [Prolific Integration](#prolific-integration)
- [CLI Commands](#cli-commands)
- [Terminology](#terminology)
- [Development](#development)
- [Citation](#citation)
- [Changelog](#changelog)
## Quick Start
Install and run locally without cloning:
```bash
pip install pearmut
# Download example campaigns
wget https://raw.githubusercontent.com/zouharvi/pearmut/refs/heads/main/examples/cesa.json
wget https://raw.githubusercontent.com/zouharvi/pearmut/refs/heads/main/examples/da.json
# Load and start
pearmut add cesa.json da.json
pearmut run
```
## Campaign Configuration
### Basic Structure
Campaigns are defined in JSON files (see [examples/](examples/)). The simplest configuration uses `task-based` assignment where each user has pre-defined tasks:
```python
{
"info": {
"assignment": "task-based",
# DA: scores
# ESA: error spans and scores
# cESA: error spans and scores, comparison of multiple translations
# MQM: error spans, categories, and scores
"protocol": "ESA",
},
"campaign_id": "wmt25_#_en-cs_CZ",
"data": [
# data for first task/user
[
[
# each evaluation item is a document
{
"instructions": "Evaluate translation from en to cs_CZ", # message to show to users above the first item
"src": "This will be the year that Guinness loses its cool. Cheers to that!",
"tgt": {"modelA": "Nevím přesně, kdy jsem to poprvé zaznamenal. Možná to bylo ve chvíli, ..."},
"item_id": "first item in first document"
},
{
"src": "I'm not sure I can remember exactly when I sensed it. Maybe it was when some...",
"tgt": {"modelA": "Tohle bude rok, kdy Guinness přijde o svůj „cool“ faktor. Na zdraví!"},
"item_id": "second item in first document"
}
...
],
# more document
...
],
# data for second task/user
[
...
],
# arbitrary number of users (each corresponds to a single URL to be shared)
]
}
```
Each item has to have `tgt` (dictionary from model names to strings, even for a single model evaluation).
Optionally, you can also include `src` (source string) and/or `ref` (reference string).
If neither `src` nor `ref` is provided, only the model outputs will be displayed.
For full Pearmut functionality (e.g. automatic statistical analysis), add `item_id` as well.
Any other keys that you add will simply be stored in the logs.
Load campaigns and start the server:
```bash
pearmut add my_campaign.json # Use -o/--overwrite to replace existing
pearmut run
```
### Assignment Types
- **`task-based`**: Each user has predefined items
- **`single-stream`**: All users draw from a shared pool (random assignment)
- **`dynamic`**: Items are dynamically assigned based on current model performance (see [Dynamic Assignment](#dynamic-assignment))
## Advanced Features
### Shuffling Model Translations
By default, Pearmut randomly shuffles the order in which models are shown per each item in order to avoid positional bias.
The `shuffle` parameter in campaign `info` controls this behavior:
```python
{
"info": {
"assignment": "task-based",
"protocol": "ESA",
"seed": 123, # For full reproducibility of shuffled models
"shuffle": true # Default: true. Set to false to disable shuffling.
},
"campaign_id": "my_campaign",
"data": [...]
}
```
Documents in `data_welcome` and `data_goodbye` are not shuffled and so don't require to have the same models in all documents.
### Showing Model Names
By default, model names are hidden to avoid biasing annotators. To display model names on top of each output block, set `show_model_names` to `true`:
```python
{
"info": {
"assignment": "task-based",
"protocol": "ESA",
"show_model_names": true # Default: false.
},
"campaign_id": "my_campaign",
"data": [...]
}
```
### Default Alignment Settings
For ESA/cESA campaigns, you can set default values for the annotator settings panel directly in campaign `info`:
```python
{
"info": {
"assignment": "task-based",
"protocol": "ESA",
"show_alignment": true, # Default: true
"word_level": false # Default: false
},
"campaign_id": "my_campaign",
"data": [...]
}
```
These act as defaults. If an annotator already has a saved preference in their browser, that saved preference is used.
### Custom Score Sliders
For multi-dimensional evaluation tasks (e.g., assessing fluency on a Likert scale), you can define custom sliders with specific ranges and steps:
```python
{
"info": {
"assignment": "task-based",
"protocol": "ESA",
"sliders": [
{"name": "Fluency", "min": 0, "max": 5, "step": 1},
{"name": "Adequacy", "min": 0, "max": 100, "step": 1}
]
},
"campaign_id": "my_campaign",
"data": [...]
}
```
When `sliders` is specified, only the custom sliders are shown. Each slider must have `name`, `min`, `max`, and `step` properties. All sliders must be answered before proceeding.
By default, cESA uses a single score slider which is colored relative to other translations of the same segment. This can be turned on or off using `slider_colors` parameter in `info`.
### Textfield comment box
To enable a textfield for commenting, post-editing or translation tasks, use the `textfield` parameter in `info`. The textfield content is stored in annotations alongside scores and error spans.
```python
{
"info": {
"protocol": "DA",
"textfield": "prefilled" # Options: null, "hidden", "visible", "prefilled"
}
}
```
**Textfield modes:**
- `null` or omitted: No textfield (default)
- `"hidden"`: Textfield hidden by default, shown by clicking a button
- `"visible"`: Textfield always visible
- `"prefilled"`: Textfield visible and pre-filled with model output for post-editing
### Custom MQM Taxonomy
For MQM protocol campaigns, you can define a custom error taxonomy instead of using the default MQM categories. Specify `mqm_categories` in the campaign `info` section as a dictionary mapping main categories to lists of subcategories:
```python
{
"info": {
"assignment": "task-based",
"protocol": "MQM",
"mqm_categories": {
"General": ["Accuracy", "Fluency"],
"Audio-specific": ["Inaudible", "Background noise", "Speaker overlap", "Misinterpretation"],
"Style": ["Awkward", "Embarassing"],
"Unknown": [] # Category with no subcategories
}
},
"campaign_id": "custom_mqm_example",
"data": [...]
}
```
If `mqm_categories` is not provided, the default MQM taxonomy will be used. The empty string key `""` provides an unselected state in the dropdown. Categories with empty subcategory lists (e.g., `"Style": []`) do not require a subcategory selection.
The severity levels can also be customized via `mqm_severities` (default: `["Minor", "Major"]`):
```json
"mqm_severities": ["Neutral", "Minor", "Major", "Critical"]
```
To allow annotating error spans without any severity levels, specify an empty list:
```json
"mqm_severities": []
```
See [examples/custom_mqm.json](examples/custom_mqm.json) for a complete example.
You can also set `"mqm_categories": "input"` so that the annotators fill in the category from scratch. Useful for explorative analyses.
### Special Tokens
By default, ESA, cESA, and MQM protocols allow annotating missing content with a `[missing]` token.
You can customize this by providing a list of strings in the `special_tokens` array inside campaign `info`.
For example: `["[missing]", "[untranslated]"]`.
### Custom Instructions
Set campaign-level instructions using the `instructions` field in `info` (supports HTML).
Instructions default to protocol-specific ones (DA: scoring, ESA: error spans + scoring, MQM: error spans + categories + scoring).
```python
{
"info": {
"protocol": "DA",
"instructions": "Rate translation quality on a 0-100 scale.
Pay special attention to document-level phenomena."
}
}
```
### Pre-filled Error Spans (ESAAI)
Include `error_spans` to pre-fill annotations that users can review, modify, or delete:
```python
{
"src": "The quick brown fox jumps over the lazy dog.",
"tgt": {"modelA": "Rychlá hnědá liška skáče přes líného psa."},
"error_spans": {
"modelA": [
{
"start_i": 0, # character index start (inclusive)
"end_i": 5, # character index end (inclusive)
"severity": "minor", # "minor", "major", "neutral", or null
"category": null # MQM category string or null
},
{
"start_i": 27,
"end_i": 32,
"severity": "major",
"category": null
}
]
}
}
```
The `error_spans` field is a 2D array (one per candidate). See [examples/esaai_prefilled.json](examples/esaai_prefilled.json).
### Tutorial and Attention Checks
Add `validation` rules for tutorials or attention checks. For tutorial items that users should be able to skip if they've seen them before, add `skippable: true` at the item level:
```python
{
"src": "The quick brown fox jumps.",
"tgt": {"modelA": "Rychlá hnědá liška skáče."},
"skippable": true, # Show skip button for incomplete items
"validation": {
"modelA": {
"warning": "Please set score between 70-80.", # shown on failure (omit for silent logging)
"score": [70, 80], # required score range [min, max]
"error_spans": [{"start_i": [0, 2], "end_i": [4, 8], "severity": "minor"}] # expected spans
}
}
}
```
**Types:**
- **Tutorial**: Include `skippable: true` and `warning` to let users skip after feedback
- **Loud attention checks**: Include `warning` without `skippable` to force retry
- **Silent attention checks**: Omit `warning` to log failures without notification (quality control)
The `validation` field 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).
**Score comparison:** Use `score_greaterthan` to ensure one candidate scores higher than another:
```python
{
"src": "AI transforms industries.",
"tgt": {"A": "UI transformuje průmysly.", "B": "Umělá inteligence mění obory."},
"validation": {
"A": [
{"warning": "A has error, score 20-40.", "score": [20, 40]}
],
"B": [
{"warning": "B is correct and must score higher than A.", "score": [70, 90], "score_greaterthan": "A"}
]
}
}
```
The `score_greaterthan` field specifies the index of the candidate that must have a lower score than the current candidate.
See [examples/tutorial/esa_deen.json](examples/tutorial/esa_deen.json) for a mock campaign with a fully prepared ESA tutorial.
To use it, simply extract the `data` attribute and prefix it to each task in your campaign.
#### Universal Tutorial Items with `data_welcome`
Use `data_welcome` to add tutorial items that users must complete before starting regular tasks. The structure is a list of documents (same as `data`). Welcome items have IDs `welcome_0`, `welcome_1`, etc. and are tracked separately via `progress_welcome`.
This works similarly with `data_goodbye`.
#### Attention Checks with `data_random`
Use `data_random` to add items that will be shown to users randomly during their annotation process (with a probability `data_random_prob` set in `info`).
This is useful for attention checks or quality control. Random items are drawn from this list such that a user will not see the same random item twice.
They are internally indexed as `random_0`, `random_1`, etc., but do not appear in the progress bar.
### Form Items for User Metadata
Collect user information (demographics, expertise) before annotation tasks using form items in `data_welcome`.
Form items have `text` (label/question) and `form` (field type: `null`, `"string"`, `"number"`, `"choices"`, and `"script"`).
Documents must be homogeneous: all form items or all evaluation items.
```python
{
"data_welcome": [
[
{"text": "What is your native language?", "form": "string"},
{"text": "Rate your expertise (1-10)", "form": "number"}
]
]
}
```
It is possible to automatically collect additional information from the host system using `"script"` field type.
Typically such a form document (or their sequence) would be stored in `"data_welcome"` such that it is both mandatory and show to all users.
See [examples/user_info_form.json](examples/user_info_form.json).
### Single-stream Assignment
All annotators draw from a shared pool with random assignment:
```python
{
"campaign_id": "my campaign 6",
"info": {
"assignment": "single-stream",
# DA: scores
# MQM: error spans and categories
# ESA: error spans and scores
"protocol": "ESA",
"users": 50, # number of annotators (can also be a list, see below)
"docs_per_user": 10, # optional: show goodbye after N documents per user
},
"data": [...], # list of all items (shared among all annotators)
}
```
Set `docs_per_user` to limit how many documents each user annotates before seeing the goodbye message (for single-stream, this is the number of documents).
### Dynamic Assignment
The `dynamic` assignment type intelligently selects items based on current model performance to focus annotation effort on top-performing models using contrastive comparisons.
All items must contain outputs from all models for this assignment type to work properly.
```python
{
"campaign_id": "my dynamic campaign",
"info": {
"assignment": "dynamic",
"protocol": "ESA",
"users": 10, # number of annotators
"dynamic_models": 2, # how many models to compare per item (optional, default: 1)
"dynamic_coldstart": 5, # annotations per model before dynamic kicks in (optional, default: 5)
"dynamic_coldstart_pool": 5, # optional: from how many of the first N items the coldstart should sample from.
# this is useful if you want to ensure that coldstarted models are evaluated on the same set
"docs_per_user": 20, # optional: show goodbye after N documents per user
},
"data": [...], # list of all items (shared among all annotators)
}
```
Set `docs_per_user` to limit how many documents each user annotates before seeing the goodbye message (for dynamic, this is roughly the number of documents × models).
**How it works:**
1. Initial phase: Each model gets `dynamic_coldstart` annotations with fully random contrastive evaluation
2. Dynamic phase: After the initial phase, models sampled according to 1/rank (by average score) are identified
3. Contrastive evaluation: From the top N models, `dynamic_models` models are randomly selected for each item
4. Item prioritization: Items with the least annotations for the selected models are prioritized
This approach efficiently focuses annotation resources on distinguishing between the best-performing models while ensuring all models get adequate baseline coverage. The contrastive evaluation allows for direct comparison of multiple models simultaneously.
For an example, see [examples/dynamic.json](examples/dynamic.json).
### Pre-defined User IDs and Tokens
The `users` field accepts:
- **Number** (e.g., `50`): Generate random user IDs
- **List of strings** (e.g., `["alice", "bob"]`): Use specific user IDs
- **List of dictionaries**: Specify custom tokens:
```python
{
"info": {
...
"users": [
{"user_id": "alice", "token_pass": "alice_done", "token_fail": "alice_fail"},
{"user_id": "bob", "token_pass": "bob_done"} # missing tokens are auto-generated
],
},
...
}
```
### Multimodal Annotations
Support for HTML-compatible elements (YouTube embeds, `