cuju-render 0.1.0__py3-none-any.whl

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 (382) hide show
  1. cuju/render/__init__.py +759 -0
  2. cuju/render/__main__.py +1065 -0
  3. cuju/render/_export/__init__.py +3 -0
  4. cuju/render/_export/feed.py +542 -0
  5. cuju/render/_export/sizes.py +157 -0
  6. cuju/render/_export/video.py +1618 -0
  7. cuju/render/_export/video_jobs.py +408 -0
  8. cuju/render/_format/__init__.py +3 -0
  9. cuju/render/_format/annotations.py +235 -0
  10. cuju/render/_format/codec.py +307 -0
  11. cuju/render/_format/keep.py +121 -0
  12. cuju/render/_format/recording.py +860 -0
  13. cuju/render/_format/writer.py +54 -0
  14. cuju/render/_guide/__init__.py +3 -0
  15. cuju/render/_guide/doctor.py +523 -0
  16. cuju/render/_guide/help.py +62 -0
  17. cuju/render/_guide/tutorial.py +254 -0
  18. cuju/render/_http/__init__.py +3 -0
  19. cuju/render/_http/commands.py +57 -0
  20. cuju/render/_http/headers.py +103 -0
  21. cuju/render/_http/launch.py +500 -0
  22. cuju/render/_http/launchers.py +408 -0
  23. cuju/render/_http/pwa.py +128 -0
  24. cuju/render/_http/server.py +2105 -0
  25. cuju/render/_http/window.py +360 -0
  26. cuju/render/_moments/__init__.py +3 -0
  27. cuju/render/_moments/captions.py +284 -0
  28. cuju/render/_moments/events.py +1123 -0
  29. cuju/render/_moments/search.py +694 -0
  30. cuju/render/_packages/__init__.py +3 -0
  31. cuju/render/_packages/glsl.py +746 -0
  32. cuju/render/_packages/loudness.py +334 -0
  33. cuju/render/_packages/pack.py +1959 -0
  34. cuju/render/_packages/presentation.py +1376 -0
  35. cuju/render/_packages/vocabulary.py +609 -0
  36. cuju/render/_playing/__init__.py +3 -0
  37. cuju/render/_playing/achievements.py +463 -0
  38. cuju/render/_playing/bench.py +609 -0
  39. cuju/render/_playing/challenges.py +1813 -0
  40. cuju/render/_playing/demo.py +323 -0
  41. cuju/render/_playing/draw.py +211 -0
  42. cuju/render/_playing/errors.py +20 -0
  43. cuju/render/_playing/game.py +645 -0
  44. cuju/render/_playing/limits.py +339 -0
  45. cuju/render/_playing/play.py +926 -0
  46. cuju/render/_playing/policy_worker.py +250 -0
  47. cuju/render/_playing/strict.py +810 -0
  48. cuju/render/_registry/__init__.py +3 -0
  49. cuju/render/_registry/clips.py +394 -0
  50. cuju/render/_registry/joints.py +108 -0
  51. cuju/render/_registry/teams.py +2058 -0
  52. cuju/render/_store/__init__.py +3 -0
  53. cuju/render/_store/home.py +70 -0
  54. cuju/render/_store/library.py +604 -0
  55. cuju/render/_store/samples.py +163 -0
  56. cuju/render/_text/__init__.py +3 -0
  57. cuju/render/_text/languages.py +637 -0
  58. cuju/render/_text/messages.py +1396 -0
  59. cuju/render/_tournaments/__init__.py +3 -0
  60. cuju/render/_tournaments/results.py +547 -0
  61. cuju/render/_tournaments/tournament.py +2742 -0
  62. cuju/render/_tournaments/tournament_cli.py +587 -0
  63. cuju/render/_tournaments/tournament_runner.py +1975 -0
  64. cuju/render/_version.py +6 -0
  65. cuju/render/_watch/__init__.py +3 -0
  66. cuju/render/_watch/grid.py +265 -0
  67. cuju/render/_watch/live.py +664 -0
  68. cuju/render/_watch/notebook.py +79 -0
  69. cuju/render/_watch/show.py +171 -0
  70. cuju/render/samples/sample-11v11.cjr +0 -0
  71. cuju/render/samples/sample-5v5.cjr +0 -0
  72. cuju/render/samples/teams/starter/main.py +33 -0
  73. cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
  74. cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
  75. cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
  76. cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
  77. cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
  78. cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
  79. cuju/render/web/assets/adboard_polygon.webp +0 -0
  80. cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
  81. cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
  82. cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
  83. cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
  84. cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
  85. cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
  86. cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
  87. cuju/render/web/assets/ball.glb +0 -0
  88. cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
  89. cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
  90. cuju/render/web/assets/challenges/corner.mp4 +0 -0
  91. cuju/render/web/assets/challenges/corner.webp +0 -0
  92. cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
  93. cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
  94. cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
  95. cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
  96. cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
  97. cuju/render/web/assets/challenges/empty_goal.webp +0 -0
  98. cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
  99. cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
  100. cuju/render/web/assets/challenges/manifest.json +375 -0
  101. cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
  102. cuju/render/web/assets/challenges/one_on_one.webp +0 -0
  103. cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
  104. cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
  105. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
  106. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
  107. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
  108. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
  109. cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
  110. cuju/render/web/assets/challenges/run_to_score.webp +0 -0
  111. cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
  112. cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
  113. cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
  114. cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
  115. cuju/render/web/assets/crowd.webp +0 -0
  116. cuju/render/web/assets/crowd_normal.webp +0 -0
  117. cuju/render/web/assets/cuju-logo-384.png +0 -0
  118. cuju/render/web/assets/cuju-logo.png +0 -0
  119. cuju/render/web/assets/cuju-logo.svg +46 -0
  120. cuju/render/web/assets/cuju-mark-light.svg +33 -0
  121. cuju/render/web/assets/cuju-mark.svg +33 -0
  122. cuju/render/web/assets/goal.glb +0 -0
  123. cuju/render/web/assets/grass.webp +0 -0
  124. cuju/render/web/assets/hair_black.png +0 -0
  125. cuju/render/web/assets/hair_blonde.png +0 -0
  126. cuju/render/web/assets/hair_brown.png +0 -0
  127. cuju/render/web/assets/hair_darkblonde.png +0 -0
  128. cuju/render/web/assets/hair_long01.glb +0 -0
  129. cuju/render/web/assets/hair_long02.glb +0 -0
  130. cuju/render/web/assets/hair_medium01.glb +0 -0
  131. cuju/render/web/assets/hair_red.png +0 -0
  132. cuju/render/web/assets/hair_short01.glb +0 -0
  133. cuju/render/web/assets/hair_short02.glb +0 -0
  134. cuju/render/web/assets/icons/cuju-192.png +0 -0
  135. cuju/render/web/assets/icons/cuju-512.png +0 -0
  136. cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
  137. cuju/render/web/assets/kit_goalkeeper.webp +0 -0
  138. cuju/render/web/assets/kit_mask.png +0 -0
  139. cuju/render/web/assets/kit_normal.webp +0 -0
  140. cuju/render/web/assets/manifest.json +601 -0
  141. cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
  142. cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
  143. cuju/render/web/assets/models/kaykit-knight.glb +0 -0
  144. cuju/render/web/assets/models/kaykit-knight.json +23 -0
  145. cuju/render/web/assets/models/kaykit-mage.glb +0 -0
  146. cuju/render/web/assets/models/kaykit-mage.json +23 -0
  147. cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
  148. cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
  149. cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
  150. cuju/render/web/assets/models/kaykit-ranger.json +23 -0
  151. cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
  152. cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
  153. cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
  154. cuju/render/web/assets/models/kaykit-rogue.json +23 -0
  155. cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
  156. cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
  157. cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
  158. cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
  159. cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
  160. cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
  161. cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
  162. cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
  163. cuju/render/web/assets/pitch.webp +0 -0
  164. cuju/render/web/assets/pitch_normal.webp +0 -0
  165. cuju/render/web/assets/player.glb +0 -0
  166. cuju/render/web/assets/shoe.webp +0 -0
  167. cuju/render/web/assets/shoe_sole.png +0 -0
  168. cuju/render/web/assets/skin_01.png +0 -0
  169. cuju/render/web/assets/skin_02.png +0 -0
  170. cuju/render/web/assets/skin_03.png +0 -0
  171. cuju/render/web/assets/skin_04.png +0 -0
  172. cuju/render/web/assets/sounds/applause.ogg +0 -0
  173. cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
  174. cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
  175. cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
  176. cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
  177. cuju/render/web/assets/sounds/city-horn.ogg +0 -0
  178. cuju/render/web/assets/sounds/city-loop.ogg +0 -0
  179. cuju/render/web/assets/sounds/city-siren.ogg +0 -0
  180. cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
  181. cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
  182. cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
  183. cuju/render/web/assets/sounds/gulls.ogg +0 -0
  184. cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
  185. cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
  186. cuju/render/web/assets/sounds/sounds.json +46 -0
  187. cuju/render/web/assets/sounds/space-beep.ogg +0 -0
  188. cuju/render/web/assets/sounds/space-hum.ogg +0 -0
  189. cuju/render/web/assets/sounds/space-radio.ogg +0 -0
  190. cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
  191. cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
  192. cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
  193. cuju/render/web/easy.css +357 -0
  194. cuju/render/web/export.html +23 -0
  195. cuju/render/web/feed.css +262 -0
  196. cuju/render/web/feed.html +28 -0
  197. cuju/render/web/flat.css +120 -0
  198. cuju/render/web/home.css +837 -0
  199. cuju/render/web/index.html +373 -0
  200. cuju/render/web/js/boot.js +50 -0
  201. cuju/render/web/js/broadcast/audio.js +2141 -0
  202. cuju/render/web/js/broadcast/celebrate.js +1238 -0
  203. cuju/render/web/js/broadcast/decisions.js +609 -0
  204. cuju/render/web/js/broadcast/hud.js +352 -0
  205. cuju/render/web/js/broadcast/show.js +3614 -0
  206. cuju/render/web/js/broadcast/transition.js +2231 -0
  207. cuju/render/web/js/broadcast/voices.js +153 -0
  208. cuju/render/web/js/capture.js +101 -0
  209. cuju/render/web/js/core/accessibility.js +90 -0
  210. cuju/render/web/js/core/api.js +429 -0
  211. cuju/render/web/js/core/comfort.js +135 -0
  212. cuju/render/web/js/core/i18n.js +705 -0
  213. cuju/render/web/js/core/icons.js +167 -0
  214. cuju/render/web/js/core/input-settings.js +156 -0
  215. cuju/render/web/js/core/keys.js +277 -0
  216. cuju/render/web/js/core/language-ui.js +125 -0
  217. cuju/render/web/js/core/menu.js +853 -0
  218. cuju/render/web/js/core/names.js +113 -0
  219. cuju/render/web/js/core/page-structure.js +110 -0
  220. cuju/render/web/js/core/scenarios.js +109 -0
  221. cuju/render/web/js/core/spoilers.js +236 -0
  222. cuju/render/web/js/core/typeahead.js +38 -0
  223. cuju/render/web/js/export/dom-picture.js +330 -0
  224. cuju/render/web/js/export/export-host.js +97 -0
  225. cuju/render/web/js/export/feed-dialog.js +191 -0
  226. cuju/render/web/js/export/feed-params.js +266 -0
  227. cuju/render/web/js/export/feed.js +363 -0
  228. cuju/render/web/js/export/mp4-mux.js +295 -0
  229. cuju/render/web/js/export/photo-core.js +313 -0
  230. cuju/render/web/js/export/photo.js +1314 -0
  231. cuju/render/web/js/export/safe-frame.js +59 -0
  232. cuju/render/web/js/export/size-fields.js +103 -0
  233. cuju/render/web/js/export/sizes.js +178 -0
  234. cuju/render/web/js/export/video-boot.js +55 -0
  235. cuju/render/web/js/export/video-browser.js +325 -0
  236. cuju/render/web/js/export/video-export.js +417 -0
  237. cuju/render/web/js/export/video-plan.js +207 -0
  238. cuju/render/web/js/export/video-range.js +185 -0
  239. cuju/render/web/js/export/webm-mux.js +198 -0
  240. cuju/render/web/js/feed-boot.js +151 -0
  241. cuju/render/web/js/feed-frame.js +101 -0
  242. cuju/render/web/js/help/doctor.js +488 -0
  243. cuju/render/web/js/help/firstrun.js +379 -0
  244. cuju/render/web/js/help/help-center.js +438 -0
  245. cuju/render/web/js/help/install-offer.js +164 -0
  246. cuju/render/web/js/help/install-state.js +104 -0
  247. cuju/render/web/js/help/lessons-core.js +207 -0
  248. cuju/render/web/js/help/lessons.js +463 -0
  249. cuju/render/web/js/help/palette.js +584 -0
  250. cuju/render/web/js/help/pwa.js +257 -0
  251. cuju/render/web/js/help/tour-core.js +241 -0
  252. cuju/render/web/js/help/tour.js +461 -0
  253. cuju/render/web/js/main.js +2513 -0
  254. cuju/render/web/js/pages/draw.js +188 -0
  255. cuju/render/web/js/pages/fieldpage.js +530 -0
  256. cuju/render/web/js/pages/home.js +521 -0
  257. cuju/render/web/js/pages/matches.js +779 -0
  258. cuju/render/web/js/pages/moments-ui.js +359 -0
  259. cuju/render/web/js/pages/queue-filters.js +63 -0
  260. cuju/render/web/js/pages/recording-groups.js +80 -0
  261. cuju/render/web/js/pages/recordings.js +1069 -0
  262. cuju/render/web/js/play/bench.js +347 -0
  263. cuju/render/web/js/play/challenges.js +1302 -0
  264. cuju/render/web/js/play/medals.js +127 -0
  265. cuju/render/web/js/play/play.js +1953 -0
  266. cuju/render/web/js/play/teasers.js +196 -0
  267. cuju/render/web/js/play/touch.js +350 -0
  268. cuju/render/web/js/play/tournament-layout.js +487 -0
  269. cuju/render/web/js/play/tournament-results.js +441 -0
  270. cuju/render/web/js/play/tournament-strict.js +560 -0
  271. cuju/render/web/js/play/tournament.js +2585 -0
  272. cuju/render/web/js/players/animate.js +972 -0
  273. cuju/render/web/js/players/clips.js +224 -0
  274. cuju/render/web/js/players/kaykit.js +162 -0
  275. cuju/render/web/js/players/kits.js +676 -0
  276. cuju/render/web/js/players/number-patch.js +82 -0
  277. cuju/render/web/js/players/players.js +2694 -0
  278. cuju/render/web/js/players/recolor.js +893 -0
  279. cuju/render/web/js/players/retarget.js +325 -0
  280. cuju/render/web/js/players/rig-numbers.js +174 -0
  281. cuju/render/web/js/presentation/animator.js +231 -0
  282. cuju/render/web/js/presentation/assign.js +85 -0
  283. cuju/render/web/js/presentation/behaviors.js +440 -0
  284. cuju/render/web/js/presentation/budget.js +209 -0
  285. cuju/render/web/js/presentation/catalog.js +1674 -0
  286. cuju/render/web/js/presentation/clocks.js +72 -0
  287. cuju/render/web/js/presentation/commands.js +301 -0
  288. cuju/render/web/js/presentation/contract.js +240 -0
  289. cuju/render/web/js/presentation/effects.js +904 -0
  290. cuju/render/web/js/presentation/flash.js +605 -0
  291. cuju/render/web/js/presentation/freeze-worklet.js +22 -0
  292. cuju/render/web/js/presentation/glsl.js +464 -0
  293. cuju/render/web/js/presentation/host.js +323 -0
  294. cuju/render/web/js/presentation/index.js +2066 -0
  295. cuju/render/web/js/presentation/loudness-worklet.js +235 -0
  296. cuju/render/web/js/presentation/loudness.js +740 -0
  297. cuju/render/web/js/presentation/moments.js +536 -0
  298. cuju/render/web/js/presentation/post.js +327 -0
  299. cuju/render/web/js/presentation/preview.js +180 -0
  300. cuju/render/web/js/presentation/protocol.js +174 -0
  301. cuju/render/web/js/presentation/reach.js +158 -0
  302. cuju/render/web/js/presentation/rigging.js +130 -0
  303. cuju/render/web/js/presentation/sandbox.js +157 -0
  304. cuju/render/web/js/presentation/shaders.js +235 -0
  305. cuju/render/web/js/presentation/stage.js +402 -0
  306. cuju/render/web/js/presentation/teamaudio.js +722 -0
  307. cuju/render/web/js/presentation/tracks.js +197 -0
  308. cuju/render/web/js/presentation/values.js +81 -0
  309. cuju/render/web/js/recording/annotation-data.js +203 -0
  310. cuju/render/web/js/recording/captions.js +491 -0
  311. cuju/render/web/js/recording/codec.js +185 -0
  312. cuju/render/web/js/recording/decode-worker.js +70 -0
  313. cuju/render/web/js/recording/decode.js +94 -0
  314. cuju/render/web/js/recording/episodes.js +46 -0
  315. cuju/render/web/js/recording/format.js +461 -0
  316. cuju/render/web/js/recording/live.js +348 -0
  317. cuju/render/web/js/recording/moments-query.js +59 -0
  318. cuju/render/web/js/recording/moments.js +149 -0
  319. cuju/render/web/js/recording/pose.js +86 -0
  320. cuju/render/web/js/recording/timeline.js +275 -0
  321. cuju/render/web/js/teams/art.js +50 -0
  322. cuju/render/web/js/teams/clip-editor.js +293 -0
  323. cuju/render/web/js/teams/editor.js +1979 -0
  324. cuju/render/web/js/teams/library.js +344 -0
  325. cuju/render/web/js/teams/painter.js +633 -0
  326. cuju/render/web/js/teams/surprise-ui.js +195 -0
  327. cuju/render/web/js/teams/surprise.js +497 -0
  328. cuju/render/web/js/teams/team-check.js +127 -0
  329. cuju/render/web/js/watch/annotations.js +270 -0
  330. cuju/render/web/js/watch/flat.js +366 -0
  331. cuju/render/web/js/watch/grid.js +436 -0
  332. cuju/render/web/js/watch/pitch2d.js +289 -0
  333. cuju/render/web/js/watch/player-tools.js +192 -0
  334. cuju/render/web/js/watch/presets.js +201 -0
  335. cuju/render/web/js/world/arenas.js +997 -0
  336. cuju/render/web/js/world/ball.js +204 -0
  337. cuju/render/web/js/world/cameras.js +982 -0
  338. cuju/render/web/js/world/crowd.js +376 -0
  339. cuju/render/web/js/world/field.js +227 -0
  340. cuju/render/web/js/world/framing.js +145 -0
  341. cuju/render/web/js/world/occlusion.js +48 -0
  342. cuju/render/web/js/world/pitch.js +703 -0
  343. cuju/render/web/js/world/scene.js +395 -0
  344. cuju/render/web/js/world/sky.js +325 -0
  345. cuju/render/web/js/world/stadium.js +1262 -0
  346. cuju/render/web/js/world/vendor-sky.js +367 -0
  347. cuju/render/web/js/world/weather.js +264 -0
  348. cuju/render/web/languages/en.json +4750 -0
  349. cuju/render/web/lessons.json +137 -0
  350. cuju/render/web/manifest.webmanifest +23 -0
  351. cuju/render/web/style.css +8399 -0
  352. cuju/render/web/sw.js +235 -0
  353. cuju/render/web/vendor/fabric/LICENSE +16 -0
  354. cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
  355. cuju/render/web/vendor/lucide/LICENSE +43 -0
  356. cuju/render/web/vendor/lucide/icons.js +141 -0
  357. cuju/render/web/vendor/three/LICENSE +21 -0
  358. cuju/render/web/vendor/three/build/three.core.js +60586 -0
  359. cuju/render/web/vendor/three/build/three.module.js +19719 -0
  360. cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
  361. cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
  362. cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
  363. cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
  364. cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
  365. cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
  366. cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
  367. cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
  368. cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
  369. cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
  370. cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
  371. cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
  372. cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
  373. cuju_render-0.1.0.dist-info/METADATA +116 -0
  374. cuju_render-0.1.0.dist-info/RECORD +382 -0
  375. cuju_render-0.1.0.dist-info/WHEEL +4 -0
  376. cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
  377. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
  378. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
  379. cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
  380. cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
  381. cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
  382. cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,408 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The video exports that the page asks the server for, one at a time, each in a child process.
5
+
6
+ Each export runs `python -m cuju.render._export.video` on a recording of the library and writes into
7
+ the folder `videos` of the renderer, where the page downloads the file. The browser that the export
8
+ opens encodes the video itself, MP4 where it encodes H.264 and WebM otherwise, so the server needs a
9
+ Chrome, Edge or Chromium and nothing beside it. The folder holds the videos
10
+ of the running server alone, the latest few, and the server empties it of older files before its
11
+ first export.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import collections
17
+ import contextlib
18
+ import json
19
+ import re
20
+ import subprocess
21
+ import threading
22
+ import time
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from cuju.errors import ArgumentError
27
+
28
+ from .._store import home as _home
29
+ from .._store import library as _library
30
+ from .._text import messages as _messages
31
+ from .._text.messages import MessageError
32
+ from . import sizes as _sizes
33
+ from . import video as _video
34
+
35
+ # The videos that the folder keeps, the latest ones, beside the export that runs.
36
+ KEPT = 3
37
+ # An export ends after this many seconds, longer than the longest range takes on a slow machine.
38
+ _EXPORT_SECONDS = 3 * 60 * 60
39
+ # The frame rates that the page's dialog offers. An export over HTTP takes any whole rate from 1
40
+ # to `_video.MAX_FPS`, as the command line does. The page offers the sizes of `_sizes`, and any
41
+ # other size with even sides within its limits.
42
+ RATES = (24, 25, 30, 50, 60)
43
+
44
+
45
+ class BusyError(_messages.Message, Exception):
46
+ """Raised when an export is asked for while another one runs, with the id of its message."""
47
+
48
+
49
+ def _seconds(value: Any, name: str) -> float:
50
+ """Returns a number of seconds from the page, or raises `ArgumentError`."""
51
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not 0 <= value < 1e6:
52
+ raise MessageError('server.video.seconds', name=name, value=repr(value))
53
+ return float(value)
54
+
55
+
56
+ def options_from(document: dict[str, Any]) -> tuple[list[str], _video.Options]:
57
+ """Returns the command line arguments of an export that the page asks for, and its options.
58
+
59
+ The page names the start and end in seconds, the camera, the size, the frame rate as a whole
60
+ number from 1 to `_video.MAX_FPS`, the quality, the scale and safe area of the graphics,
61
+ whether the scoreboard and the annotations show and the arena and hour of its field.
62
+
63
+ Raises:
64
+ ArgumentError: If a field is missing, unknown or invalid.
65
+ """
66
+ known = {
67
+ 'start',
68
+ 'end',
69
+ 'camera',
70
+ 'width',
71
+ 'height',
72
+ 'fps',
73
+ 'quality',
74
+ 'score',
75
+ 'annotations',
76
+ 'arena',
77
+ 'time',
78
+ 'scale',
79
+ 'safe',
80
+ }
81
+ unknown = set(document) - known
82
+ if unknown:
83
+ raise MessageError(
84
+ 'server.video.fields', known=', '.join(sorted(known)), unknown=sorted(unknown)
85
+ )
86
+ start = _seconds(document.get('start', 0), 'start')
87
+ end = _seconds(document.get('end'), 'end')
88
+ if end <= start:
89
+ raise MessageError('server.video.range', start=f'{start:g}', end=f'{end:g}')
90
+ size = _sizes.check_size(document.get('width', 1280), document.get('height', 720), even=True)
91
+ graphics = document.get('scale', 1.0)
92
+ if isinstance(graphics, bool) or not isinstance(graphics, (int, float)):
93
+ raise MessageError('server.video.scale', graphics=repr(graphics))
94
+ safe = document.get('safe', 'auto')
95
+ if isinstance(safe, bool) or not isinstance(safe, (str, int, float)):
96
+ raise MessageError('server.video.safe', safe=repr(safe))
97
+ fps = document.get('fps', _video.DEFAULT_FPS)
98
+ if isinstance(fps, bool) or not isinstance(fps, int) or not 1 <= fps <= _video.MAX_FPS:
99
+ raise MessageError('server.video.rate', most=_video.MAX_FPS, fps=repr(fps))
100
+ score = document.get('score', True)
101
+ if not isinstance(score, bool):
102
+ raise MessageError('server.video.score', score=repr(score))
103
+ annotations = document.get('annotations', False)
104
+ if not isinstance(annotations, bool):
105
+ raise MessageError('server.video.annotations', annotations=repr(annotations))
106
+ options = _video.Options(
107
+ start=_video.Moment(start),
108
+ end=_video.Moment(end),
109
+ camera=str(document.get('camera', 'broadcast')),
110
+ width=size[0],
111
+ height=size[1],
112
+ fps=fps,
113
+ quality=str(document.get('quality', 'standard')),
114
+ score=score,
115
+ annotations=annotations,
116
+ arena=None if document.get('arena') is None else str(document['arena']),
117
+ hour=document.get('time'),
118
+ graphics=graphics,
119
+ safe=safe,
120
+ ).check()
121
+ arguments = [
122
+ '--start',
123
+ f'{start:.3f}',
124
+ '--end',
125
+ f'{end:.3f}',
126
+ '--camera',
127
+ options.camera,
128
+ '--size',
129
+ f'{options.width}x{options.height}',
130
+ '--fps',
131
+ str(options.fps),
132
+ '--quality',
133
+ options.quality,
134
+ ]
135
+ if options.graphics != 1:
136
+ arguments += ['--scale', f'{options.graphics:g}']
137
+ if options.safe != 'auto':
138
+ arguments += ['--safe', str(options.safe)]
139
+ if not options.score:
140
+ arguments.append('--no-score')
141
+ if options.annotations:
142
+ arguments.append('--annotations')
143
+ if options.arena is not None:
144
+ arguments += ['--arena', options.arena]
145
+ if options.hour is not None:
146
+ arguments += ['--time', repr(float(options.hour))]
147
+ return arguments, options
148
+
149
+
150
+ def availability() -> dict[str, Any]:
151
+ """Returns whether this machine can export videos, and what is missing when it cannot.
152
+
153
+ The export needs a browser alone, which encodes the video itself, so ffmpeg is never missing.
154
+ `missing` holds the English text of the missing browser, and `missing_labels` the same as
155
+ labels of `_messages.label`, which the page shows in its own language.
156
+ """
157
+ try:
158
+ labels = [] if _video.find_chrome() else [_video.chrome_missing(server=True)]
159
+ except _video.VideoError as error:
160
+ found = _messages.id_fields(error)
161
+ labels = [
162
+ _messages.label(found['error_id'], **found['error_values'])
163
+ if found
164
+ else {'id': None, 'text': str(error), 'values': {}}
165
+ ]
166
+ return {
167
+ 'available': not labels,
168
+ 'missing': [label['text'] for label in labels],
169
+ 'missing_labels': labels,
170
+ }
171
+
172
+
173
+ def _filename(title: str, start: float, end: float) -> str:
174
+ """Returns the name that a downloaded video suggests, from the title and the range."""
175
+ stem = re.sub(r'[^A-Za-z0-9 _.,-]+', '', title).strip(' .') or 'match'
176
+ first, last = (f'{int(value) // 60}m{int(value) % 60:02d}' for value in (start, end))
177
+ return f'{stem[:60]} {first}-{last}.mp4'
178
+
179
+
180
+ class VideoJobs:
181
+ """The exports of the page, one running at a time, each in a child process.
182
+
183
+ The folder is touched on the first export alone, so a server that never exports leaves it as
184
+ it was.
185
+ """
186
+
187
+ def __init__(self) -> None:
188
+ self._lock = threading.Lock()
189
+ self._entries: list[dict[str, Any]] = []
190
+ self._process: subprocess.Popen | None = None
191
+ self._cleaned = False
192
+
193
+ def _folder(self) -> Path:
194
+ folder = _home.folder('videos')
195
+ if not self._cleaned:
196
+ # The videos of an earlier server have no entry to download them by.
197
+ for path in folder.iterdir():
198
+ if path.is_file():
199
+ with contextlib.suppress(OSError):
200
+ path.unlink()
201
+ self._cleaned = True
202
+ return folder
203
+
204
+ @staticmethod
205
+ def _public(entry: dict[str, Any]) -> dict[str, Any]:
206
+ return {key: value for key, value in entry.items() if not key.startswith('_')}
207
+
208
+ def entries(self) -> list[dict[str, Any]]:
209
+ """Returns a copy of every export, newest first."""
210
+ with self._lock:
211
+ return [self._public(entry) for entry in reversed(self._entries)]
212
+
213
+ def entry(self, identifier: str) -> dict[str, Any] | None:
214
+ """Returns a copy of one export, or `None`."""
215
+ with self._lock:
216
+ found = next((e for e in self._entries if e['id'] == identifier), None)
217
+ return None if found is None else self._public(found)
218
+
219
+ def file(self, identifier: str) -> tuple[Path, str] | None:
220
+ """Returns the file and the suggested name of a finished export, or `None`."""
221
+ with self._lock:
222
+ found = next((e for e in self._entries if e['id'] == identifier), None)
223
+ if found is None or found['state'] != 'done':
224
+ return None
225
+ path = Path(found['_path'])
226
+ return (path, found['filename']) if path.is_file() else None
227
+
228
+ def start(self, match: str, document: dict[str, Any]) -> dict[str, Any]:
229
+ """Starts an export of a recording of the library and returns its entry.
230
+
231
+ Raises:
232
+ ArgumentError: If the recording is unknown or an option is invalid.
233
+ BusyError: If another export runs.
234
+ """
235
+ path = _library.path_of(match)
236
+ if path is None:
237
+ raise MessageError('server.video.unknown_match', match=repr(match))
238
+ arguments, options = options_from(document)
239
+ try:
240
+ facts = _video.recording_facts(path)
241
+ except _video.VideoError as error:
242
+ raise _messages.argument_error(error) from error
243
+ start, end = _video.resolve_range(options, facts)
244
+ if end - start > _video.DEFAULT_MAX_SECONDS:
245
+ raise MessageError(
246
+ 'server.video.length',
247
+ most=f'{_video.DEFAULT_MAX_SECONDS:g}',
248
+ length=f'{end - start:.0f}',
249
+ )
250
+ status = availability()
251
+ if not status['available']:
252
+ labels = status.get('missing_labels') or []
253
+ if labels and all(label.get('id') for label in labels):
254
+ found = _video.missing_label(labels)
255
+ raise MessageError(found['id'], **found['values'])
256
+ raise ArgumentError(' '.join(status['missing']))
257
+ with self._lock:
258
+ if self._process is not None and self._process.poll() is None:
259
+ raise BusyError('server.video.busy')
260
+ folder = self._folder()
261
+ identifier = _home.new_identifier('v')
262
+ output = folder / f'{identifier}.mp4'
263
+ entry = {
264
+ 'id': identifier,
265
+ 'match': match,
266
+ 'title': facts.title,
267
+ 'start': start,
268
+ 'end': end,
269
+ 'state': 'running',
270
+ 'progress': 0.0,
271
+ 'frame': 0,
272
+ 'frames': _video.frame_count(start, end, options.fps),
273
+ 'error': None,
274
+ 'error_id': None,
275
+ 'error_values': None,
276
+ 'bytes': None,
277
+ 'filename': _filename(facts.title, start, end),
278
+ 'created': time.time(),
279
+ '_path': str(output),
280
+ }
281
+ command = [
282
+ *_home.python_command(),
283
+ '-m',
284
+ 'cuju.render._export.video',
285
+ match,
286
+ '-o',
287
+ str(output),
288
+ '--format',
289
+ 'auto',
290
+ ]
291
+ # The child runs in the renderer's folder, so no module beside the server's working
292
+ # folder shadows cuju, and reads its cancellation from a pipe of its own.
293
+ process = subprocess.Popen(
294
+ [*command, *arguments],
295
+ stdin=subprocess.PIPE,
296
+ stdout=subprocess.PIPE,
297
+ stderr=subprocess.PIPE,
298
+ text=True,
299
+ encoding='utf-8',
300
+ errors='replace',
301
+ cwd=_home.process_folder(),
302
+ env=_home.process_environment(),
303
+ )
304
+ self._process = process
305
+ self._entries.append(entry)
306
+ self._prune()
307
+ threading.Thread(target=self._watch, args=(entry, process), daemon=True).start()
308
+ return self._public(entry)
309
+
310
+ def _prune(self) -> None:
311
+ """Removes the files and entries of older exports beyond `KEPT`, with the lock held."""
312
+ finished = [e for e in self._entries if e['state'] != 'running']
313
+ for entry in finished[: max(0, len(finished) - KEPT)]:
314
+ with contextlib.suppress(OSError):
315
+ Path(entry['_path']).unlink(missing_ok=True)
316
+ self._entries.remove(entry)
317
+
318
+ @staticmethod
319
+ def _moved(entry: dict[str, Any], reported: Any) -> None:
320
+ """Follows the file of an export to the suffix of the container that it was written in.
321
+
322
+ The child process names the file it wrote, which keeps the folder and the name of the
323
+ export and may take the suffix of another container, with the lock held.
324
+ """
325
+ if not isinstance(reported, str):
326
+ return
327
+ planned = Path(entry['_path'])
328
+ written = Path(reported)
329
+ if written.parent == planned.parent and written.stem == planned.stem:
330
+ if written.suffix in _video.SUFFIXES.values():
331
+ entry['_path'] = str(written)
332
+ entry['filename'] = str(Path(entry['filename']).with_suffix(written.suffix))
333
+
334
+ def _watch(self, entry: dict[str, Any], process: subprocess.Popen) -> None:
335
+ """Follows the reports of an export's process until it ends."""
336
+ stderr: collections.deque[str] = collections.deque(maxlen=20)
337
+ threading.Thread(target=lambda: stderr.extend(process.stderr), daemon=True).start()
338
+ timer = threading.Timer(_EXPORT_SECONDS, process.kill)
339
+ timer.daemon = True
340
+ timer.start()
341
+ try:
342
+ for line in process.stdout:
343
+ try:
344
+ report = json.loads(line)
345
+ except ValueError:
346
+ continue
347
+ if not isinstance(report, dict):
348
+ continue
349
+ with self._lock:
350
+ if isinstance(report.get('progress'), (int, float)):
351
+ entry['progress'] = float(report['progress'])
352
+ if isinstance(report.get('frame'), int):
353
+ entry['frame'] = report['frame']
354
+ if isinstance(report.get('error'), str):
355
+ entry.update(_messages.report_fields(report))
356
+ if report.get('stage') == 'encoding':
357
+ entry['progress'] = 1.0
358
+ if isinstance(report.get('bytes'), int):
359
+ entry['bytes'] = report['bytes']
360
+ self._moved(entry, report.get('path'))
361
+ code = process.wait()
362
+ finally:
363
+ timer.cancel()
364
+ with self._lock:
365
+ if entry['state'] == 'running':
366
+ if code == 0 and Path(entry['_path']).is_file():
367
+ entry['state'] = 'done'
368
+ elif entry.get('_cancelled'):
369
+ entry['state'] = 'cancelled'
370
+ entry.update(_messages.entry_fields(None))
371
+ else:
372
+ entry['state'] = 'failed'
373
+ if not entry['error']:
374
+ tail = ''.join(stderr).strip()[-400:]
375
+ entry.update(_messages.exit_fields('server.video.exit', code, tail))
376
+ if self._process is process:
377
+ self._process = None
378
+
379
+ def cancel(self, identifier: str) -> bool:
380
+ """Cancels a running export, or removes a finished one and its file.
381
+
382
+ A running export reads the cancellation, closes Chrome and removes its partial file, and
383
+ one that does not end within half a minute is ended.
384
+ """
385
+ with self._lock:
386
+ entry = next((e for e in self._entries if e['id'] == identifier), None)
387
+ if entry is None:
388
+ return False
389
+ if entry['state'] != 'running':
390
+ with contextlib.suppress(OSError):
391
+ Path(entry['_path']).unlink(missing_ok=True)
392
+ self._entries.remove(entry)
393
+ return True
394
+ entry['_cancelled'] = True
395
+ process = self._process
396
+ if process is not None:
397
+ with contextlib.suppress(OSError, ValueError):
398
+ process.stdin.write('cancel\n')
399
+ process.stdin.flush()
400
+
401
+ def end() -> None:
402
+ try:
403
+ process.wait(30)
404
+ except subprocess.TimeoutExpired:
405
+ process.kill()
406
+
407
+ threading.Thread(target=end, daemon=True).start()
408
+ return True
@@ -0,0 +1,3 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """The recording format, its codec, the recorder's writer, filters and annotations."""
@@ -0,0 +1,235 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Annotations, the numbers and text that a training loop or a policy attaches to steps.
5
+
6
+ An annotation belongs to a frame and, optionally, to one player of the header. It holds named
7
+ numbers such as a value estimate or a reward, a short text, the chosen action and the most likely
8
+ actions with their probabilities. A recording of version 2 keeps them in a section of their own,
9
+ and a live source streams them beside its frames. Annotating reads nothing of the batch and
10
+ changes nothing of it.
11
+
12
+ A record keeps short keys, as the file stores them. `f` is the frame, `p` the player, `v` the
13
+ named numbers, `t` the text, `a` the action and `k` the likely actions, each a pair of an action
14
+ and its probability, the most likely first.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import math
21
+ import struct
22
+ from collections.abc import Iterable, Mapping
23
+ from typing import Any
24
+
25
+ from cuju.errors import ArgumentError
26
+
27
+ TAG = b'ANNO'
28
+ # The limits of one annotation.
29
+ MAX_VALUES = 16
30
+ NAME_LENGTH = 40
31
+ TEXT_LENGTH = 200
32
+ MAX_ACTIONS = 1000
33
+ # The likely actions that an annotation keeps.
34
+ TOP = 3
35
+
36
+
37
+ def _number(value: Any, what: str) -> float | int:
38
+ """Returns a finite number from a Python, numpy or torch scalar, to six significant digits."""
39
+ if hasattr(value, 'item') and not isinstance(value, int | float):
40
+ value = value.item()
41
+ if isinstance(value, bool) or not isinstance(value, int | float) or not math.isfinite(value):
42
+ raise ArgumentError(f'expected {what} as a finite number, found {value!r}')
43
+ rounded = float(f'{value:.6g}')
44
+ return int(rounded) if rounded.is_integer() and abs(rounded) < 2**53 else rounded
45
+
46
+
47
+ def player_index(lineup: list[dict[str, Any]], player: Any) -> int:
48
+ """Returns the index in the header of a player given by number or as a pair of team and index.
49
+
50
+ A number counts the players of the left team first, then those of the right team, each team in
51
+ the order of its indices, as the rewards and observations of a batch count them. The header may
52
+ list the teams in another order, which the returned index follows.
53
+ """
54
+ if isinstance(player, int) and not isinstance(player, bool):
55
+ if 0 <= player < len(lineup):
56
+ order = sorted(
57
+ range(len(lineup)),
58
+ key=lambda k: (lineup[k].get('team', 0), lineup[k].get('index', k), k),
59
+ )
60
+ return order[player]
61
+ elif isinstance(player, tuple | list) and len(player) == 2:
62
+ for index, entry in enumerate(lineup):
63
+ if [entry.get('team'), entry.get('index')] == list(player):
64
+ return index
65
+ raise ArgumentError(
66
+ f'expected a player from 0 to {len(lineup) - 1} or a pair of team and index, '
67
+ f'found {player!r}'
68
+ )
69
+
70
+
71
+ def player_number(lineup: list[dict[str, Any]], index: int) -> int:
72
+ """Returns the number of the player at `index` of the header, the left team's players first.
73
+
74
+ The number is the one that `player_index` reads, so a player given by number comes back as
75
+ that number whatever order the header lists the teams in.
76
+ """
77
+ order = sorted(
78
+ range(len(lineup)),
79
+ key=lambda k: (lineup[k].get('team', 0), lineup[k].get('index', k), k),
80
+ )
81
+ return order.index(index) if 0 <= index < len(lineup) else index
82
+
83
+
84
+ def _likely(probabilities: Any) -> list[list[float | int]]:
85
+ """Returns the most likely actions with their probabilities, from a sequence or a mapping."""
86
+ if hasattr(probabilities, 'tolist'):
87
+ probabilities = probabilities.tolist()
88
+ if isinstance(probabilities, Mapping):
89
+ pairs = list(probabilities.items())
90
+ elif isinstance(probabilities, Iterable) and not isinstance(probabilities, str | bytes):
91
+ pairs = list(enumerate(probabilities))
92
+ else:
93
+ raise ArgumentError(
94
+ f'expected probabilities as a sequence or mapping, found {probabilities!r}'
95
+ )
96
+ if len(pairs) > MAX_ACTIONS:
97
+ raise ArgumentError(f'expected at most {MAX_ACTIONS} probabilities, found {len(pairs)}')
98
+ checked = []
99
+ for action, probability in pairs:
100
+ if isinstance(action, bool) or not isinstance(action, int) or action < 0:
101
+ raise ArgumentError(f'expected actions as whole numbers, found {action!r}')
102
+ checked.append((action, _number(probability, 'a probability')))
103
+ checked.sort(key=lambda pair: (-pair[1], pair[0]))
104
+ return [[action, float(f'{probability:.4g}')] for action, probability in checked[:TOP]]
105
+
106
+
107
+ def note(
108
+ lineup: list[dict[str, Any]],
109
+ values: Mapping[str, Any] | None = None,
110
+ text: str | None = None,
111
+ player: Any = None,
112
+ probabilities: Any = None,
113
+ action: Any = None,
114
+ ) -> dict[str, Any]:
115
+ """Returns the record of one annotation without its frame, after checking every part.
116
+
117
+ Raises:
118
+ ArgumentError: If a part is invalid or the annotation holds nothing.
119
+ """
120
+ record: dict[str, Any] = {}
121
+ if player is not None:
122
+ record['p'] = player_index(lineup, player)
123
+ if values is not None:
124
+ if not isinstance(values, Mapping) or len(values) > MAX_VALUES:
125
+ raise ArgumentError(f'expected values as a mapping of at most {MAX_VALUES} names')
126
+ named = {}
127
+ for name, value in values.items():
128
+ if not isinstance(name, str) or not 0 < len(name) <= NAME_LENGTH:
129
+ raise ArgumentError(
130
+ f'expected names of 1 to {NAME_LENGTH} characters, found {name!r}'
131
+ )
132
+ named[name] = _number(value, f'the value {name!r}')
133
+ if named:
134
+ record['v'] = named
135
+ if text is not None:
136
+ if not isinstance(text, str):
137
+ raise ArgumentError(f'expected text as a string, found {text!r}')
138
+ record['t'] = text[:TEXT_LENGTH]
139
+ if action is not None:
140
+ if hasattr(action, 'item'):
141
+ action = action.item()
142
+ if isinstance(action, bool) or not isinstance(action, int) or action < 0:
143
+ raise ArgumentError(f'expected the action as a whole number, found {action!r}')
144
+ record['a'] = action
145
+ if probabilities is not None:
146
+ record['k'] = _likely(probabilities)
147
+ if not record.keys() - {'p'}:
148
+ raise ArgumentError('expected values, text, an action or probabilities to annotate')
149
+ return record
150
+
151
+
152
+ def merge(
153
+ records: dict[tuple[int, int], dict[str, Any]], frame: int, record: dict[str, Any]
154
+ ) -> dict[str, Any]:
155
+ """Adds an annotation of `frame` to `records`, merged with an earlier one of the same player.
156
+
157
+ Named numbers join those already there, and the text, action and likely actions replace
158
+ theirs. Returns the merged record.
159
+
160
+ Raises:
161
+ ArgumentError: If the merged record would hold more than `MAX_VALUES` names.
162
+ """
163
+ key = (frame, record.get('p', -1))
164
+ merged = dict(records.get(key) or {'f': frame})
165
+ for name, value in record.items():
166
+ merged[name] = {**merged.get('v', {}), **value} if name == 'v' else value
167
+ if len(merged.get('v', ())) > MAX_VALUES:
168
+ raise ArgumentError(f'expected at most {MAX_VALUES} named values on a step')
169
+ records[key] = merged
170
+ return merged
171
+
172
+
173
+ def frame_of(frames: list[bytes], step: int | None) -> int:
174
+ """Returns the index of the frame of `step`, the last frame for `None`.
175
+
176
+ Raises:
177
+ ArgumentError: If no frame holds the step.
178
+ """
179
+ if step is not None:
180
+ if isinstance(step, bool) or not isinstance(step, int):
181
+ raise ArgumentError(f'expected the step as a whole number, found {step!r}')
182
+ for index in range(len(frames) - 1, -1, -1):
183
+ if struct.unpack_from('<i', frames[index])[0] == step:
184
+ return index
185
+ raise ArgumentError(f'expected a step of the current episode, found {step}')
186
+ return len(frames) - 1
187
+
188
+
189
+ def encode(records: Iterable[dict[str, Any]]) -> bytes:
190
+ """Returns the payload of an annotations section, its records in order of frame and player."""
191
+ ordered = sorted(records, key=lambda record: (record['f'], record.get('p', -1)))
192
+ return json.dumps({'records': ordered}, separators=(',', ':')).encode()
193
+
194
+
195
+ def decode(payload: bytes) -> list[dict[str, Any]]:
196
+ """Returns the records of an annotations section, leaving out any that is malformed."""
197
+ try:
198
+ records = json.loads(payload)['records']
199
+ except (ValueError, KeyError, TypeError, RecursionError):
200
+ return []
201
+ return (
202
+ [record for record in records if _frame(record) is not None]
203
+ if isinstance(records, list)
204
+ else []
205
+ )
206
+
207
+
208
+ def _frame(record: Any) -> int | None:
209
+ """Returns the frame of a record, a whole number from 0, or `None` for a malformed record."""
210
+ frame = record.get('f') if isinstance(record, dict) else None
211
+ whole = isinstance(frame, int) and not isinstance(frame, bool)
212
+ return frame if whole and frame >= 0 else None
213
+
214
+
215
+ def public(
216
+ record: dict[str, Any], frames: list[bytes], lineup: list[dict[str, Any]] | None = None
217
+ ) -> dict[str, Any]:
218
+ """Returns a record with long keys and the step of its frame, as a filter reads it.
219
+
220
+ With the header's `lineup`, the player is its number with the left team's players first, as
221
+ `annotate` takes it, and without one the index in the header.
222
+ """
223
+ frame = record['f']
224
+ player = record.get('p')
225
+ if lineup is not None and isinstance(player, int):
226
+ player = player_number(lineup, player)
227
+ return {
228
+ 'frame': frame,
229
+ 'step': struct.unpack_from('<i', frames[frame])[0] if 0 <= frame < len(frames) else None,
230
+ 'player': player,
231
+ 'values': dict(record.get('v', {})),
232
+ 'text': record.get('t'),
233
+ 'action': record.get('a'),
234
+ 'probabilities': [list(pair) for pair in record.get('k', ())],
235
+ }