scanpath-studio 0.34.0__tar.gz → 0.35.0__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 (152) hide show
  1. {scanpath_studio-0.34.0/scanpath_studio.egg-info → scanpath_studio-0.35.0}/PKG-INFO +24 -21
  2. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/README.md +21 -18
  3. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/pyproject.toml +2 -2
  4. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/__init__.py +17 -9
  5. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/aggregation.py +17 -23
  6. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/analysis_recipe.py +3 -3
  7. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/animation_export.py +12 -12
  8. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/annotations.py +25 -12
  9. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/api.py +384 -139
  10. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/app.py +706 -217
  11. scanpath_studio-0.35.0/scanpath_studio/assets/scanpath_studio_title_logo.png +0 -0
  12. scanpath_studio-0.35.0/scanpath_studio/assets/scanpath_studio_title_logo_dark.png +0 -0
  13. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/authoring.py +12 -7
  14. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/authoring_component.py +1 -1
  15. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/cli.py +531 -229
  16. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/code_snippet.py +163 -64
  17. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/column_names.py +110 -13
  18. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/compare_source.py +2 -2
  19. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/computations.py +117 -119
  20. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/constants.py +28 -3
  21. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/controls.py +892 -371
  22. scanpath_studio-0.35.0/scanpath_studio/crash_report.py +97 -0
  23. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/data.py +523 -89
  24. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/data_health.py +5 -5
  25. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/dataset_table.py +7 -6
  26. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/datasets.py +16 -13
  27. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/debug_log.py +22 -19
  28. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/experimental_setup.py +9 -9
  29. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/export.py +129 -35
  30. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/export_status.py +2 -1
  31. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/fields.py +21 -3
  32. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/html_embed.py +14 -2
  33. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/illustration.py +10 -3
  34. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/measures.py +15 -38
  35. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/menu.py +4 -9
  36. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/multipart.py +28 -14
  37. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/persistence.py +34 -5
  38. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/plots.py +491 -97
  39. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/session_keys.py +71 -4
  40. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/similarity.py +13 -22
  41. scanpath_studio-0.35.0/scanpath_studio/streamlit_entry.py +12 -0
  42. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/styles.py +153 -44
  43. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/synthetic.py +8 -9
  44. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/tabs.py +1444 -458
  45. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/tour.py +228 -189
  46. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/url_state.py +309 -58
  47. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/utils.py +74 -20
  48. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/wizard.py +186 -83
  49. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/wizard_shell.py +5 -7
  50. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0/scanpath_studio.egg-info}/PKG-INFO +24 -21
  51. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio.egg-info/SOURCES.txt +6 -0
  52. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio.egg-info/requires.txt +1 -1
  53. scanpath_studio-0.34.0/scanpath_studio/assets/scanpath_studio_title_logo.png +0 -0
  54. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/LICENSE +0 -0
  55. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/MANIFEST.in +0 -0
  56. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/NOTICE +0 -0
  57. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/__main__.py +0 -0
  58. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/alignment.py +0 -0
  59. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/easter_egg.py +0 -0
  60. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/eyegenbench.py +0 -0
  61. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/eyegenbench_geometry.py +0 -0
  62. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/loading.py +0 -0
  63. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/metadata.py +0 -0
  64. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/model_scanpaths.py +0 -0
  65. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/onestop_shard.py +0 -0
  66. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/preprocessing.py +0 -0
  67. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/progress.py +0 -0
  68. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/fixations.csv +0 -0
  69. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/fixations.parquet +0 -0
  70. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/ia.csv +0 -0
  71. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/ia.parquet +0 -0
  72. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_a.png +0 -0
  73. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_b.png +0 -0
  74. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_c.png +0 -0
  75. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_d.png +0 -0
  76. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__paragraph.png +0 -0
  77. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__question.png +0 -0
  78. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_a.png +0 -0
  79. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_b.png +0 -0
  80. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_c.png +0 -0
  81. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_d.png +0 -0
  82. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__paragraph.png +0 -0
  83. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__question.png +0 -0
  84. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_a.png +0 -0
  85. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_b.png +0 -0
  86. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_c.png +0 -0
  87. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_d.png +0 -0
  88. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__paragraph.png +0 -0
  89. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__question.png +0 -0
  90. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_a.png +0 -0
  91. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_b.png +0 -0
  92. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_c.png +0 -0
  93. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_d.png +0 -0
  94. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__paragraph.png +0 -0
  95. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__question.png +0 -0
  96. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_a.png +0 -0
  97. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_b.png +0 -0
  98. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_c.png +0 -0
  99. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_d.png +0 -0
  100. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__paragraph.png +0 -0
  101. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__question.png +0 -0
  102. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_a.png +0 -0
  103. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_b.png +0 -0
  104. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_c.png +0 -0
  105. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_d.png +0 -0
  106. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__paragraph.png +0 -0
  107. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__question.png +0 -0
  108. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_a.png +0 -0
  109. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_b.png +0 -0
  110. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_c.png +0 -0
  111. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_d.png +0 -0
  112. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__paragraph.png +0 -0
  113. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__question.png +0 -0
  114. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_a.png +0 -0
  115. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_b.png +0 -0
  116. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_c.png +0 -0
  117. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_d.png +0 -0
  118. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__paragraph.png +0 -0
  119. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__question.png +0 -0
  120. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_a.png +0 -0
  121. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_b.png +0 -0
  122. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_c.png +0 -0
  123. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_d.png +0 -0
  124. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__paragraph.png +0 -0
  125. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__question.png +0 -0
  126. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_a.png +0 -0
  127. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_b.png +0 -0
  128. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_c.png +0 -0
  129. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_d.png +0 -0
  130. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__paragraph.png +0 -0
  131. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__question.png +0 -0
  132. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_a.png +0 -0
  133. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_b.png +0 -0
  134. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_c.png +0 -0
  135. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_d.png +0 -0
  136. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__paragraph.png +0 -0
  137. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__question.png +0 -0
  138. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_a.png +0 -0
  139. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_b.png +0 -0
  140. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_c.png +0 -0
  141. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_d.png +0 -0
  142. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__paragraph.png +0 -0
  143. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__question.png +0 -0
  144. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/manifest.csv +0 -0
  145. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/raw_gaze.csv +0 -0
  146. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/raw_gaze.parquet +0 -0
  147. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/truncation_tooltip.py +0 -0
  148. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/update_sample_data.py +0 -0
  149. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio.egg-info/dependency_links.txt +0 -0
  150. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio.egg-info/entry_points.txt +0 -0
  151. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio.egg-info/top_level.txt +0 -0
  152. {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/setup.cfg +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scanpath-studio
3
- Version: 0.34.0
4
- Summary: Interactive Streamlit workbench for visualizing eye-tracking-while-reading scanpaths, computing reading measures, and exporting figures and tabular data.
3
+ Version: 0.35.0
4
+ Summary: Interactive workbench for eye-tracking-while-reading data: draw, replay and compare scanpaths over the text, and export figures and tables.
5
5
  Author: Keren Gruteke Klein, Maya Grossman, Ella Lion, Deborah N. Jakobi, David R. Reich, Lena Jäger, Yevgeni Berzak
6
6
  Author-email: Omer Shubi <lacclab.technion@gmail.com>
7
7
  License-Expression: MIT
@@ -23,7 +23,7 @@ Requires-Python: >=3.11
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  License-File: NOTICE
26
- Requires-Dist: streamlit>=1.64.0
26
+ Requires-Dist: streamlit>=1.65.0
27
27
  Requires-Dist: pandas>=3.0
28
28
  Requires-Dist: plotly>=7.1
29
29
  Requires-Dist: numpy>=2.4
@@ -65,16 +65,18 @@ Dynamic: license-file
65
65
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22933884.svg)](https://doi.org/10.5281/zenodo.22933884)
66
66
 
67
67
  **Scanpath Studio shows you how people read.** Load eye-tracking-while-reading
68
- data and watch each reading unfold over the text, exactly where it sat on the
69
- screen — then compare readers, analyse a corpus, and export figures ready for a
70
- paper.
68
+ data and watch each trial unfold over the text, exactly where it sat on the
69
+ screen — then compare participants, analyze a corpus, and export figures ready
70
+ for a paper.
71
71
 
72
- ![Using Scanpath Studio: stepping through trials, a heatmap, a replay, a two-reader comparison and Corpus Analysis](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/docs/assets/app_demo.gif)
72
+ ![Using Scanpath Studio: stepping through trials, a heatmap, a replay, a two-participant comparison and Corpus Analysis](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/docs/assets/app_demo.gif)
73
73
 
74
74
  ## Get started
75
75
 
76
- - **In the browser:** the live demo at
77
- <https://scanpath-studio.streamlit.app>.
76
+ - **As a desktop app**, the easiest way to work with your own data:
77
+ [Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64.zip) ·
78
+ [macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.dmg) ·
79
+ [Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
78
80
  - **With pip** (Python 3.11–3.14):
79
81
 
80
82
  ```bash
@@ -82,30 +84,31 @@ paper.
82
84
  scanpath-studio # opens the app in your browser
83
85
  ```
84
86
 
85
- - **As a desktop app:** [Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64.zip) ·
86
- [macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.dmg) ·
87
- [Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
87
+ - **In the browser**, to try it on the bundled demo data: the live demo at
88
+ <https://scanpath-studio.streamlit.app>.
88
89
 
89
- The pip and desktop installs keep your data on your own machine; the hosted demo
90
- runs on Streamlit Community Cloud.
90
+ The desktop and pip installs keep your data on your own machine, handle large
91
+ datasets, and download the public corpora (PoTeC, OneStop) in one click. The
92
+ hosted demo runs on Streamlit Community Cloud, with limited memory and no
93
+ corpus downloads.
91
94
 
92
95
  ## What you can do
93
96
 
94
97
  - **See the reading:** fixations, saccades, heatmaps and raw gaze over the text
95
98
  at its true on-screen position, with fixations colored by any column.
96
99
  - **Replay it** in real time or faster, and export it as HTML, GIF or MP4.
97
- - **Compare readers:** overlay two trials or place them side by side — even
100
+ - **Compare participants:** overlay two trials or place them side by side — even
98
101
  from two different datasets.
99
- - **Analyse a corpus** per text, reader or group, with every measure
100
- documented in the
102
+ - **Analyze a corpus** per text, participant or group, from the reading
103
+ measures your data brings, each defined in the
101
104
  [computation register](https://lacclab.github.io/scanpath-studio/computations/).
102
105
  - **Triage, export and share:** tag and filter trials, export one figure or a
103
106
  zip for every trial, and share a link that reopens the exact view.
104
107
 
105
108
  | | |
106
109
  |:---:|:---:|
107
- | ![A reading scanpath replayed fixation by fixation](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/assets/scanpath_animation.gif) | ![Two readers of the same paragraph, overlaid on one canvas](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/assets/demo_dual_scanpath.png) |
108
- | A reading, replayed fixation by fixation | Two readers of one paragraph, overlaid ([animated](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/docs/assets/demo_dual_scanpath.gif)) |
110
+ | ![A scanpath replayed fixation by fixation](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/assets/scanpath_animation.gif) | ![Two participants reading the same paragraph, overlaid on one canvas](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/assets/demo_dual_scanpath.png) |
111
+ | A trial, replayed fixation by fixation | Two participants on one paragraph, overlaid ([animated](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/docs/assets/demo_dual_scanpath.gif)) |
109
112
 
110
113
  The app has three views: 🗺️ **Scanpath** for one trial at a time, 📊 **Corpus
111
114
  Analysis** for the whole dataset, and 🗂️ **Data Management** for loading and configuring
@@ -143,8 +146,8 @@ sps.save_figure(fig, "scanpath.html")
143
146
 
144
147
  For your own files, pass `--words ia.csv --fixations fix.csv` to `render`, or
145
148
  use `sps.load_scanpath_data("ia.csv", "fix.csv")`. HTML output needs nothing
146
- else; PNG, SVG, PDF, GIF and MP4 go through Kaleido, which needs Chrome once:
147
- `plotly_get_chrome -y`. The
149
+ else; PNG, SVG and PDF (and GIF/MP4 replays) go through Kaleido, which needs
150
+ Chrome, Chromium or Edge, or run `plotly_get_chrome -y` once. The
148
151
  [CLI reference](https://lacclab.github.io/scanpath-studio/cli/) and the
149
152
  [Python API reference](https://lacclab.github.io/scanpath-studio/api/) list
150
153
  every flag and parameter.
@@ -11,16 +11,18 @@
11
11
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22933884.svg)](https://doi.org/10.5281/zenodo.22933884)
12
12
 
13
13
  **Scanpath Studio shows you how people read.** Load eye-tracking-while-reading
14
- data and watch each reading unfold over the text, exactly where it sat on the
15
- screen — then compare readers, analyse a corpus, and export figures ready for a
16
- paper.
14
+ data and watch each trial unfold over the text, exactly where it sat on the
15
+ screen — then compare participants, analyze a corpus, and export figures ready
16
+ for a paper.
17
17
 
18
- ![Using Scanpath Studio: stepping through trials, a heatmap, a replay, a two-reader comparison and Corpus Analysis](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/docs/assets/app_demo.gif)
18
+ ![Using Scanpath Studio: stepping through trials, a heatmap, a replay, a two-participant comparison and Corpus Analysis](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/docs/assets/app_demo.gif)
19
19
 
20
20
  ## Get started
21
21
 
22
- - **In the browser:** the live demo at
23
- <https://scanpath-studio.streamlit.app>.
22
+ - **As a desktop app**, the easiest way to work with your own data:
23
+ [Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64.zip) ·
24
+ [macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.dmg) ·
25
+ [Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
24
26
  - **With pip** (Python 3.11–3.14):
25
27
 
26
28
  ```bash
@@ -28,30 +30,31 @@ paper.
28
30
  scanpath-studio # opens the app in your browser
29
31
  ```
30
32
 
31
- - **As a desktop app:** [Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64.zip) ·
32
- [macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.dmg) ·
33
- [Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
33
+ - **In the browser**, to try it on the bundled demo data: the live demo at
34
+ <https://scanpath-studio.streamlit.app>.
34
35
 
35
- The pip and desktop installs keep your data on your own machine; the hosted demo
36
- runs on Streamlit Community Cloud.
36
+ The desktop and pip installs keep your data on your own machine, handle large
37
+ datasets, and download the public corpora (PoTeC, OneStop) in one click. The
38
+ hosted demo runs on Streamlit Community Cloud, with limited memory and no
39
+ corpus downloads.
37
40
 
38
41
  ## What you can do
39
42
 
40
43
  - **See the reading:** fixations, saccades, heatmaps and raw gaze over the text
41
44
  at its true on-screen position, with fixations colored by any column.
42
45
  - **Replay it** in real time or faster, and export it as HTML, GIF or MP4.
43
- - **Compare readers:** overlay two trials or place them side by side — even
46
+ - **Compare participants:** overlay two trials or place them side by side — even
44
47
  from two different datasets.
45
- - **Analyse a corpus** per text, reader or group, with every measure
46
- documented in the
48
+ - **Analyze a corpus** per text, participant or group, from the reading
49
+ measures your data brings, each defined in the
47
50
  [computation register](https://lacclab.github.io/scanpath-studio/computations/).
48
51
  - **Triage, export and share:** tag and filter trials, export one figure or a
49
52
  zip for every trial, and share a link that reopens the exact view.
50
53
 
51
54
  | | |
52
55
  |:---:|:---:|
53
- | ![A reading scanpath replayed fixation by fixation](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/assets/scanpath_animation.gif) | ![Two readers of the same paragraph, overlaid on one canvas](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/assets/demo_dual_scanpath.png) |
54
- | A reading, replayed fixation by fixation | Two readers of one paragraph, overlaid ([animated](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/docs/assets/demo_dual_scanpath.gif)) |
56
+ | ![A scanpath replayed fixation by fixation](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/assets/scanpath_animation.gif) | ![Two participants reading the same paragraph, overlaid on one canvas](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/assets/demo_dual_scanpath.png) |
57
+ | A trial, replayed fixation by fixation | Two participants on one paragraph, overlaid ([animated](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/docs/assets/demo_dual_scanpath.gif)) |
55
58
 
56
59
  The app has three views: 🗺️ **Scanpath** for one trial at a time, 📊 **Corpus
57
60
  Analysis** for the whole dataset, and 🗂️ **Data Management** for loading and configuring
@@ -89,8 +92,8 @@ sps.save_figure(fig, "scanpath.html")
89
92
 
90
93
  For your own files, pass `--words ia.csv --fixations fix.csv` to `render`, or
91
94
  use `sps.load_scanpath_data("ia.csv", "fix.csv")`. HTML output needs nothing
92
- else; PNG, SVG, PDF, GIF and MP4 go through Kaleido, which needs Chrome once:
93
- `plotly_get_chrome -y`. The
95
+ else; PNG, SVG and PDF (and GIF/MP4 replays) go through Kaleido, which needs
96
+ Chrome, Chromium or Edge, or run `plotly_get_chrome -y` once. The
94
97
  [CLI reference](https://lacclab.github.io/scanpath-studio/cli/) and the
95
98
  [Python API reference](https://lacclab.github.io/scanpath-studio/api/) list
96
99
  every flag and parameter.
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "scanpath-studio"
7
7
  dynamic = ["version"]
8
- description = "Interactive Streamlit workbench for visualizing eye-tracking-while-reading scanpaths, computing reading measures, and exporting figures and tabular data."
8
+ description = "Interactive workbench for eye-tracking-while-reading data: draw, replay and compare scanpaths over the text, and export figures and tables."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
11
11
  authors = [
@@ -34,7 +34,7 @@ classifiers = [
34
34
  "Topic :: Scientific/Engineering :: Visualization",
35
35
  ]
36
36
  dependencies = [
37
- "streamlit>=1.64.0",
37
+ "streamlit>=1.65.0",
38
38
  "pandas>=3.0",
39
39
  "plotly>=7.1",
40
40
  "numpy>=2.4",
@@ -5,15 +5,12 @@ from __future__ import annotations
5
5
  __all__ = [
6
6
  "ScanpathData",
7
7
  "__version__",
8
- "alignment_sensitivity",
9
- "analysis_tables",
10
8
  "animate_scanpath",
11
9
  "build_authored_scanpath",
12
10
  "cache_status",
13
11
  "check_data_health",
14
12
  "clear_cache",
15
13
  "compare_scanpaths",
16
- "compute_word_metrics",
17
14
  "figure_code",
18
15
  "figure_options",
19
16
  "list_parts",
@@ -31,15 +28,12 @@ __all__ = [
31
28
  "main",
32
29
  "plot_corpus_figure",
33
30
  "plot_scanpath",
34
- "preprocess_data",
35
31
  "propose_schema",
36
- "reader_summary",
37
32
  "render_parent_trial",
38
33
  "save_figure",
39
34
  "save_figure_layers",
40
- "trial_summary",
41
35
  ]
42
- __version__ = "0.34.0"
36
+ __version__ = "0.35.0"
43
37
 
44
38
  # Public headless API (see api.py / datasets.py / eyegenbench.py). Resolved lazily so
45
39
  # `import scanpath_studio` stays cheap and doesn't pull in pandas/plotly/
@@ -47,10 +41,23 @@ __version__ = "0.34.0"
47
41
  # The MultiplEYE and benchmark-corpus loaders stay importable but are left out
48
42
  # of `__all__`: those corpora are held back from this release
49
43
  # (`constants.multipleye_enabled` / `benchmark_corpora_enabled`).
44
+ # The app's own computed measures are held back the same way
45
+ # (`constants.computed_measures_enabled`): importable, but not advertised —
46
+ # they raise without SCANPATH_EXPERIMENTAL=1 (#374).
47
+ _HELD_BACK_EXPORTS = frozenset(
48
+ {
49
+ "alignment_sensitivity",
50
+ "analysis_tables",
51
+ "compute_word_metrics",
52
+ "preprocess_data",
53
+ "reader_summary",
54
+ "trial_summary",
55
+ }
56
+ )
50
57
  _DATASET_EXPORTS = frozenset({"load_potec", "load_multipleye", "load_onestop"})
51
58
  _EYEGENBENCH_EXPORTS = frozenset({"load_eyegenbench", "eyegenbench_datasets"})
52
59
  _API_EXPORTS = (
53
- frozenset(__all__)
60
+ (frozenset(__all__) | _HELD_BACK_EXPORTS)
54
61
  - {"__version__", "main"}
55
62
  - _DATASET_EXPORTS
56
63
  - _EYEGENBENCH_EXPORTS
@@ -78,7 +85,8 @@ def __dir__() -> list:
78
85
 
79
86
 
80
87
  def main() -> None:
81
- """Programmatic entry point — `from scanpath_studio import main`."""
88
+ """The app's Streamlit script body. To open the app, run `scanpath-studio`;
89
+ from Python, use the headless API instead."""
82
90
  from .app import main as _main
83
91
 
84
92
  _main()
@@ -127,7 +127,7 @@ MEASURES: dict[str, Measure] = {
127
127
  ),
128
128
  Measure(
129
129
  "fprt",
130
- "First-pass gaze — FPRT",
130
+ "First-pass reading time — FPRT",
131
131
  "words",
132
132
  "first_pass_gaze_duration_ms",
133
133
  "ms",
@@ -171,7 +171,7 @@ MEASURES: dict[str, Measure] = {
171
171
  ),
172
172
  Measure(
173
173
  "landing_distance",
174
- "Centred landing distance",
174
+ "Centered landing distance",
175
175
  "words",
176
176
  "initial_landing_distance",
177
177
  "letters",
@@ -737,7 +737,7 @@ def reader_vs_cohort_values(
737
737
  *,
738
738
  normalize: bool = False,
739
739
  ) -> dict[str, np.ndarray]:
740
- """``{"This reader": …, "Cohort": …}`` value arrays for a measure (AN-7)."""
740
+ """``{"This participant": …, "Cohort": …}`` value arrays for a measure (AN-7)."""
741
741
  if frame is None or frame.empty or "participant_id" not in frame.columns:
742
742
  return {}
743
743
  work = frame.copy()
@@ -749,7 +749,7 @@ def reader_vs_cohort_values(
749
749
  me = work.loc[is_target, "_m"].dropna().to_numpy()
750
750
  others = work.loc[~is_target, "_m"].dropna().to_numpy()
751
751
  if me.size:
752
- out["This reader"] = me
752
+ out["This participant"] = me
753
753
  if others.size:
754
754
  out["Cohort"] = others
755
755
  return out
@@ -1286,7 +1286,7 @@ def landing_positions(
1286
1286
  way the letter position is: counted from where the glyphs end.
1287
1287
 
1288
1288
  Not clipped: a first fixation the word got although it lies outside the box
1289
- horizontally (the 50 px nearest-word fallback, or an imported ``word_id``)
1289
+ horizontally (an imported ``word_id``)
1290
1290
  reads below 0 or above 1, rather than piling onto an edge it did not land on.
1291
1291
  """
1292
1292
  from .measures import word_glyph_span
@@ -1578,18 +1578,21 @@ def paired_group_summary(
1578
1578
  spec_b: Mapping,
1579
1579
  *,
1580
1580
  agg: str = "mean",
1581
- spread: str = "SEM",
1582
1581
  label_a: str = "Group A",
1583
1582
  label_b: str = "Group B",
1584
1583
  words: pd.DataFrame | None = None,
1585
1584
  fixations: pd.DataFrame | None = None,
1586
1585
  normalize: bool = False,
1587
1586
  ) -> pd.DataFrame:
1588
- """Per-measure group means + error for the paired bars (AN-20).
1589
-
1590
- Returns ``[measure, group, value, err_lo, err_hi, n]``. ``measures`` may mix
1591
- word- and fixation-level measures; pass ``words``/``fixations`` so each reads
1592
- its backing frame (``frame`` is the fallback).
1587
+ """Per-measure group summaries for the paired bars (AN-20).
1588
+
1589
+ Returns ``[measure, group, value, n_observations]``: ``value`` is ``agg``
1590
+ over every word or fixation of the group pooled across participants, and
1591
+ ``n_observations`` counts those pooled values. No error bars this release
1592
+ (#374): a spread over pooled words from the same few participants reads as
1593
+ a precision the data does not have. ``measures`` may mix word- and
1594
+ fixation-level measures; pass ``words``/``fixations`` so each reads its
1595
+ backing frame (``frame`` is the fallback).
1593
1596
  """
1594
1597
  label_a, label_b = distinct_group_labels(label_a, label_b)
1595
1598
  rows = []
@@ -1603,24 +1606,15 @@ def paired_group_summary(
1603
1606
  continue
1604
1607
  for spec, label in ((spec_a, label_a), (spec_b, label_b)):
1605
1608
  vals = measure_values(apply_group(src, spec), m, normalize=normalize)
1606
- center = aggregate_value(vals, agg)
1607
- lo, hi = spread_bounds(vals, center, spread, agg=agg)
1608
1609
  rows.append(
1609
1610
  {
1610
1611
  "measure": m.label,
1611
1612
  "group": label,
1612
- # Clamp to ≥0: an asymmetric band (e.g. mean + IQR on skewed
1613
- # data) can put the centre outside [lo, hi], and a negative
1614
- # Plotly error-bar length renders in the wrong direction.
1615
- "value": center,
1616
- "err_lo": max(center - lo, 0.0),
1617
- "err_hi": max(hi - center, 0.0),
1618
- "n": int(vals.size),
1613
+ "value": aggregate_value(vals, agg),
1614
+ "n_observations": int(vals.size),
1619
1615
  }
1620
1616
  )
1621
- return pd.DataFrame(
1622
- rows, columns=["measure", "group", "value", "err_lo", "err_hi", "n"]
1623
- )
1617
+ return pd.DataFrame(rows, columns=["measure", "group", "value", "n_observations"])
1624
1618
 
1625
1619
 
1626
1620
  def group_mean_difference(
@@ -35,8 +35,8 @@ RECIPE_VERSION = 1
35
35
  #: What the recipe deliberately leaves out, written into every one.
36
36
  EXCLUDES = {
37
37
  "figure_settings": (
38
- "Palette, canvas and fonts are not recorded here. Save them with "
39
- "Share → File, which holds no trial filters."
38
+ "Palette, canvas and fonts live in the settings file (Scanpath → "
39
+ "Share → File), which holds no trial filters."
40
40
  ),
41
41
  "data": (
42
42
  "No table rows and no annotation notes. The dataset is referenced by "
@@ -133,7 +133,7 @@ def analysis_choices(
133
133
  if normalize is not None:
134
134
  rate = bool(getattr(measure, "is_rate", False))
135
135
  out["normalization"] = (
136
- "z-score within reader" if normalize and not rate else "none"
136
+ "z-score per participant" if normalize and not rate else "none"
137
137
  )
138
138
  if spread:
139
139
  out["spread"] = spread
@@ -121,7 +121,7 @@ class AnimationBudgetError(AnimationExportError):
121
121
  # `chromium_browser_path` finds an installed Chrome, Chromium or Edge there too.
122
122
  CHROME_INSTALL_HINT = (
123
123
  "Image export needs Chrome, Chromium or Edge, and none was found. Install one "
124
- "of them and try again — a pip install can instead run `plotly_get_chrome -y`. "
124
+ "of them and try again — or, with a pip install, run `plotly_get_chrome -y`. "
125
125
  "The **HTML** export needs no browser."
126
126
  )
127
127
 
@@ -130,7 +130,7 @@ def chromium_browser_path() -> str | None:
130
130
  """Return the browser path Kaleido would use, without launching it.
131
131
 
132
132
  ``Chromium.find_browser`` is Choreographer's complete discovery path: it
133
- honours ``BROWSER_PATH``, searches ``PATH``, checks platform-specific
133
+ honors ``BROWSER_PATH``, searches ``PATH``, checks platform-specific
134
134
  locations for Chrome, Chromium, Edge, Brave, and Vivaldi, and can fall back
135
135
  to Choreographer's managed Chrome download. We prefer a system browser: a
136
136
  stale managed download must not shadow a working installed Edge/Chrome.
@@ -187,7 +187,7 @@ def _static_base(fig: go.Figure) -> go.Figure:
187
187
  truncate the existing entries), so we assign the attributes directly. The
188
188
  reserved control band is then reclaimed so the clip isn't topped by an empty
189
189
  strip; a slim margin remains for the "Elapsed" annotation. Only the control
190
- band is reclaimed: the other margins hold a title, a caption, a colour bar or
190
+ band is reclaimed: the other margins hold a title, a caption, a color bar or
191
191
  the coordinate-grid ticks, and are kept as they are. The replay's clock
192
192
  on ``layout.meta`` (BUG-93) goes too — only the live player reads it, and it
193
193
  would otherwise ride into every frame Kaleido renders.
@@ -286,14 +286,14 @@ def check_gif_budget(
286
286
  if total <= budget:
287
287
  return
288
288
  fits = int(budget) // per_frame
289
- shorter = f"cap it at {fits} frames or fewer, " if fits >= 1 else ""
289
+ shorter = f"cap it at {fits} frames or fewer, or " if fits >= 1 else ""
290
290
  raise AnimationBudgetError(
291
291
  f"a {n_frames}-frame GIF at {width}×{height} px and {scale:g}× comes to "
292
292
  f"{total / 1e6:,.0f} megapixels of frames, over the "
293
293
  f"{budget / 1e6:,.0f}-megapixel limit for one GIF"
294
294
  f"{' on this shared server' if shared else ''}. Export **MP4** instead — "
295
295
  f"its frames stream straight to the encoder and the file stays small — or "
296
- f"{shorter}or lower the resolution."
296
+ f"{shorter}lower the resolution."
297
297
  )
298
298
 
299
299
 
@@ -406,7 +406,8 @@ def render_png_frames(
406
406
  raise AnimationExportError(
407
407
  CHROME_INSTALL_HINT
408
408
  if not chrome_available()
409
- else f"Rendering frame {k + 1}/{len(frames)} failed: {exc}."
409
+ else f"Frame {k + 1} of {len(frames)} couldn't be drawn "
410
+ f"({exc}). Try again, or export the HTML replay."
410
411
  ) from exc
411
412
  pngs.append(bytes(png))
412
413
  if progress_callback is not None:
@@ -432,7 +433,7 @@ def encode_gif(
432
433
  ) -> bytes:
433
434
  """Encode PNG frames into an animated GIF with a uniform per-frame delay.
434
435
 
435
- **Streams** (SEC1 / BUG-74): each PNG is decoded, quantized to a 256-colour
436
+ **Streams** (SEC1 / BUG-74): each PNG is decoded, quantized to a 256-color
436
437
  palette and written before the next is read, so memory holds one decoded frame
437
438
  and one palette frame, not the whole clip. Pillow's ``save(save_all=True)``
438
439
  cannot do that — it keeps every normalized frame until the end to diff them —
@@ -556,8 +557,7 @@ def encode_mp4(pngs: list[bytes], frame_duration_ms: float) -> bytes:
556
557
  raise
557
558
  except Exception as exc:
558
559
  raise AnimationExportError(
559
- f"MP4 encoding failed: {exc}. Try the GIF format, or check that "
560
- "imageio-ffmpeg is installed."
560
+ f"The MP4 couldn't be made ({exc}). Try GIF, or the HTML replay."
561
561
  ) from exc
562
562
  finally:
563
563
  try:
@@ -580,12 +580,12 @@ def export_animation(
580
580
  """Render a scanpath-animation figure to GIF or MP4 bytes.
581
581
 
582
582
  Args:
583
- fig: the figure from :func:`make_scanpath_animation` (must have ``.frames``).
583
+ fig: a replay from ``scanpath_studio.animate_scanpath`` (it must have frames).
584
584
  fmt: ``"gif"`` or ``"mp4"``.
585
585
  frame_duration_ms: uniform per-frame duration. By default the replay's
586
586
  own (:func:`plots.animation_clip_frame_ms`), so the clip lasts what
587
587
  the on-screen replay does — ``reading span / playback speed``.
588
- Required for a figure :func:`make_scanpath_animation` didn't build.
588
+ Required for a figure ``animate_scanpath`` didn't build.
589
589
  scale: Kaleido render scale (1.0 = on-screen px; <1 is faster/smaller,
590
590
  >1 is crisper/larger).
591
591
  show_elapsed: draw the "Elapsed: X.Xs" readout in the top margin.
@@ -679,7 +679,7 @@ def export_animation(
679
679
  emit_status(
680
680
  status_callback,
681
681
  ExportStage.FINALIZING,
682
- "Finalizing animation bytes…",
682
+ "Finishing the animation…",
683
683
  started_at=started,
684
684
  )
685
685
  result = bytes(result)
@@ -172,7 +172,7 @@ def _check_record(index: int, record: object) -> None:
172
172
  not isinstance(tags, list)
173
173
  or any(isinstance(t, bool) or not isinstance(t, _ID_TYPES) for t in tags)
174
174
  ):
175
- raise AnnotationsFileError(f"{where}: tags must be a list of words")
175
+ raise AnnotationsFileError(f"{where}: tags must be a list of text labels")
176
176
  note = record.get("note", "")
177
177
  if note is not None and not isinstance(note, str):
178
178
  raise AnnotationsFileError(f"{where}: note must be text")
@@ -851,10 +851,14 @@ def render_trial_annotations(
851
851
  "radio",
852
852
  "Annotation scope",
853
853
  options=["Parent trial", "This screen"],
854
+ # #374: the stored option keeps its name; "parent" is internal.
855
+ format_func=lambda option: (
856
+ "Whole trial" if option == "Parent trial" else option
857
+ ),
854
858
  key=scope_key,
855
859
  horizontal=True,
856
- help="Parent annotations follow the logical trial; screen annotations "
857
- "describe only the active coordinate space.",
860
+ help="Whole trial: every screen of this trial. This screen: only the "
861
+ "screen on view.",
858
862
  )
859
863
  if scope == "This screen":
860
864
  annotation_screen = str(screen_id)
@@ -897,7 +901,7 @@ def render_trial_annotations(
897
901
  f"{ICONS['favorite']} Favorite (star this trial)",
898
902
  display=f"{ICONS['favorite']} Favorite",
899
903
  key=star_key,
900
- help="Mark this trial as a favorite.",
904
+ help="Star this trial (with scope *This screen*, this screen only).",
901
905
  on_change=_save_entry_callback,
902
906
  args=save_args,
903
907
  )
@@ -1137,11 +1141,18 @@ def _delete_dataset_annotations(records: list[dict]) -> None:
1137
1141
  _refresh_dataset_widgets(f"Deleted {_plural(removed, 'annotation')}.")
1138
1142
 
1139
1143
 
1140
- def _annotations_frame(records: list[dict], trials: frozenset) -> pd.DataFrame:
1144
+ def _annotations_frame(
1145
+ records: list[dict], trials: frozenset, trial_labels=None
1146
+ ) -> pd.DataFrame:
1147
+ """The Annotations table. ``trial_labels`` maps a trial id to the trial
1148
+ picker's label for it (#374 F5: one trial label everywhere)."""
1149
+ shown = trial_labels or {}
1141
1150
  frame = pd.DataFrame(
1142
1151
  {
1143
1152
  "Participant": [r["participant_id"] for r in records],
1144
- "Trial": [r["trial_id"] for r in records],
1153
+ "Trial": [
1154
+ shown.get(str(r["trial_id"]), str(r["trial_id"])) for r in records
1155
+ ],
1145
1156
  "Screen": [r.get("screen_id", "") for r in records],
1146
1157
  "Favorite": [r["star"] for r in records],
1147
1158
  "Tags": [r["tags"] for r in records],
@@ -1159,7 +1170,9 @@ def _annotations_frame(records: list[dict], trials: frozenset) -> pd.DataFrame:
1159
1170
  return frame
1160
1171
 
1161
1172
 
1162
- def render_dataset_annotations(trials, *, dataset_name: str, open_trials=None) -> None:
1173
+ def render_dataset_annotations(
1174
+ trials, *, dataset_name: str, open_trials=None, trial_labels=None
1175
+ ) -> None:
1163
1176
  """🗂️ Data → **Annotations**: every annotation the open dataset holds.
1164
1177
 
1165
1178
  One table — participant, trial, favorite, tags, note — with **Export** (this
@@ -1178,7 +1191,8 @@ def render_dataset_annotations(trials, *, dataset_name: str, open_trials=None) -
1178
1191
 
1179
1192
  ``open_trials`` — the trials the Scanpath picker can show, after the trial
1180
1193
  filters — gives each row an **Open** button (:func:`_open_annotation`);
1181
- without it the table has none.
1194
+ without it the table has none. ``trial_labels`` writes each trial as
1195
+ the trial picker does.
1182
1196
  """
1183
1197
  trials = _trial_set(trials)
1184
1198
  records = store_to_records(_store())
@@ -1232,12 +1246,11 @@ def render_dataset_annotations(trials, *, dataset_name: str, open_trials=None) -
1232
1246
  st.caption(
1233
1247
  f"{_plural(elsewhere, 'annotation')} here "
1234
1248
  f"{'is' if elsewhere == 1 else 'are'} on trials this dataset hasn't "
1235
- "loaded (*In dataset* unticked) — from an earlier load of it, or "
1236
- "saved before annotations were kept per dataset. They stay with this "
1249
+ "loaded (*In dataset* unticked). They stay with this "
1237
1250
  "dataset; to move them to another, **Export** them here and "
1238
1251
  "**Import** them there."
1239
1252
  )
1240
- frame = _annotations_frame(records, trials)
1253
+ frame = _annotations_frame(records, trials, trial_labels)
1241
1254
  column_config = {}
1242
1255
  if open_trials is not None:
1243
1256
  frame.insert(0, "Open", _OPEN_LABEL)
@@ -1246,7 +1259,7 @@ def render_dataset_annotations(trials, *, dataset_name: str, open_trials=None) -
1246
1259
  "",
1247
1260
  type="tertiary",
1248
1261
  width="small",
1249
- help="Show this reading — and its screen, for a screen annotation — "
1262
+ help="Show this trial — and its screen, for a screen annotation — "
1250
1263
  "in the Scanpath view.",
1251
1264
  on_click=_open_annotation,
1252
1265
  args=(open_key, records, trials, _trial_set(open_trials)),