# 🍐Pearmut
[![PyPi version](https://badgen.net/pypi/v/pearmut/)](https://pypi.org/project/pearmut) [![PyPI download/month](https://img.shields.io/pypi/dm/pearmut.svg)](https://pypi.python.org/pypi/pearmut/) [![PyPi license](https://badgen.net/pypi/license/pearmut/)](https://pypi.org/project/pearmut/) [![build status](https://github.com/zouharvi/pearmut/actions/workflows/test.yml/badge.svg)](https://github.com/zouharvi/pearmut/actions/workflows/test.yml) [![arXiv](https://img.shields.io/badge/arXiv-2601.02933-b31b1b.svg?style=flat)](https://arxiv.org/abs/2601.02933) [![trailer](https://img.shields.io/badge/🎥%20trailer-F00.svg)](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!). Screenshot of ESA/MQM interface ## 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"} ] ] } ``` Screenshot of a user form 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, `