@the-inclusionist/engine 10.0.0 → 11.0.0

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 (528) hide show
  1. package/README.md +32 -1
  2. package/app/css/style.css +216 -42
  3. package/app/public/vendor/fonts-licences/NOTICE.txt +166 -0
  4. package/app/public/vendor/fonts-licences/OFL-1.1.txt +86 -0
  5. package/app/public/vendor/fonts.css +11 -1175
  6. package/dist-pkg/boot/create-game.d.ts +84 -33
  7. package/dist-pkg/boot/create-game.js +708 -307
  8. package/dist-pkg/core/a11y-sr.d.ts +1 -1
  9. package/dist-pkg/core/a11y-sr.js +3 -5
  10. package/dist-pkg/core/accommodations.d.ts +26 -8
  11. package/dist-pkg/core/accommodations.js +27 -10
  12. package/dist-pkg/core/actions.d.ts +39 -10
  13. package/dist-pkg/core/actions.js +28 -6
  14. package/dist-pkg/core/contract.d.ts +0 -2
  15. package/dist-pkg/core/contract.js +0 -2
  16. package/dist-pkg/core/i18n.d.ts +36 -71
  17. package/dist-pkg/core/i18n.js +131 -198
  18. package/dist-pkg/core/session-clock.d.ts +23 -0
  19. package/dist-pkg/core/session-clock.js +37 -0
  20. package/dist-pkg/i18n/en.js +44 -49
  21. package/dist-pkg/i18n/es.js +44 -49
  22. package/dist-pkg/i18n/pt.js +49 -54
  23. package/dist-pkg/input/gamepad.d.ts +42 -11
  24. package/dist-pkg/input/gamepad.js +50 -15
  25. package/dist-pkg/input/key-default.d.ts +30 -0
  26. package/dist-pkg/input/key-default.js +60 -0
  27. package/dist-pkg/input/keyboard-runtime.d.ts +0 -4
  28. package/dist-pkg/input/keyboard.d.ts +0 -1
  29. package/dist-pkg/input/keydown.d.ts +5 -11
  30. package/dist-pkg/input/keydown.js +3 -3
  31. package/dist-pkg/input/latch-edge.d.ts +10 -2
  32. package/dist-pkg/input/latch-edge.js +4 -3
  33. package/dist-pkg/input/latch-scope.d.ts +27 -22
  34. package/dist-pkg/input/latch-scope.js +25 -37
  35. package/dist-pkg/input/latch-store.d.ts +9 -12
  36. package/dist-pkg/input/latch-store.js +13 -18
  37. package/dist-pkg/input/latch-sync.d.ts +5 -4
  38. package/dist-pkg/input/latch-sync.js +5 -5
  39. package/dist-pkg/input/pad-reading.js +7 -2
  40. package/dist-pkg/input/pad-wizard.d.ts +4 -1
  41. package/dist-pkg/input/pad-wizard.js +60 -9
  42. package/dist-pkg/input/switch-scan.d.ts +29 -12
  43. package/dist-pkg/input/switch-scan.js +15 -2
  44. package/dist-pkg/input/touch-bindings.d.ts +3 -6
  45. package/dist-pkg/input/touch.d.ts +17 -2
  46. package/dist-pkg/input/touch.js +55 -17
  47. package/dist-pkg/input/transport-in-use.d.ts +5 -5
  48. package/dist-pkg/input/transport-in-use.js +5 -5
  49. package/dist-pkg/input/transports.d.ts +0 -9
  50. package/dist-pkg/input/transports.js +1 -1
  51. package/dist-pkg/input/virtual-controller.d.ts +39 -3
  52. package/dist-pkg/input/virtual-controller.js +32 -4
  53. package/dist-pkg/input/voice-map.d.ts +27 -11
  54. package/dist-pkg/input/voice-map.js +41 -43
  55. package/dist-pkg/platform/audio-ambient.d.ts +0 -1
  56. package/dist-pkg/platform/audio-ambient.js +3 -1
  57. package/dist-pkg/platform/audio-mixer.js +4 -9
  58. package/dist-pkg/platform/audio-sonar.d.ts +8 -94
  59. package/dist-pkg/platform/audio-sonar.js +22 -207
  60. package/dist-pkg/platform/audio.d.ts +10 -1
  61. package/dist-pkg/platform/audio.js +35 -2
  62. package/dist-pkg/platform/choose-by-voice.d.ts +70 -0
  63. package/dist-pkg/platform/choose-by-voice.js +157 -0
  64. package/dist-pkg/platform/font-library.d.ts +77 -0
  65. package/dist-pkg/platform/font-library.js +98 -0
  66. package/dist-pkg/platform/font-library.json +721 -0
  67. package/dist-pkg/platform/heavy-catalogue.d.ts +46 -0
  68. package/dist-pkg/platform/heavy-catalogue.js +47 -14
  69. package/dist-pkg/platform/heavy-mirror.d.ts +3 -9
  70. package/dist-pkg/platform/heavy-mirror.js +14 -17
  71. package/dist-pkg/platform/heavy.d.ts +40 -8
  72. package/dist-pkg/platform/heavy.js +177 -22
  73. package/dist-pkg/platform/interruptible-speech.d.ts +15 -8
  74. package/dist-pkg/platform/interruptible-speech.js +37 -13
  75. package/dist-pkg/platform/listener-scope.js +22 -13
  76. package/dist-pkg/platform/locale-host.d.ts +7 -3
  77. package/dist-pkg/platform/locale-host.js +13 -2
  78. package/dist-pkg/platform/reading-in-worker.d.ts +24 -0
  79. package/dist-pkg/platform/reading-in-worker.js +38 -0
  80. package/dist-pkg/platform/reading-model.d.ts +21 -0
  81. package/dist-pkg/platform/reading-model.js +25 -0
  82. package/dist-pkg/platform/reading-runtime.js +13 -8
  83. package/dist-pkg/platform/reading.d.ts +12 -1
  84. package/dist-pkg/platform/reading.js +9 -1
  85. package/dist-pkg/platform/speech-recognition.d.ts +10 -1
  86. package/dist-pkg/platform/speech-recognition.js +29 -9
  87. package/dist-pkg/platform/speech.d.ts +10 -1
  88. package/dist-pkg/platform/speech.js +24 -13
  89. package/dist-pkg/platform/tts.d.ts +37 -4
  90. package/dist-pkg/platform/tts.js +279 -57
  91. package/dist-pkg/platform/voice-listener.d.ts +6 -2
  92. package/dist-pkg/platform/voice-listener.js +1 -1
  93. package/dist-pkg/platform/voice-plan.d.ts +13 -0
  94. package/dist-pkg/platform/voice-plan.js +57 -2
  95. package/dist-pkg/platform/vosk-runtime.d.ts +25 -0
  96. package/dist-pkg/platform/vosk-runtime.js +59 -1
  97. package/dist-pkg/platform/vosk-vocabulary.d.ts +6 -0
  98. package/dist-pkg/platform/vosk-vocabulary.js +178 -0
  99. package/dist-pkg/render/crt.d.ts +11 -9
  100. package/dist-pkg/render/crt.js +6 -10
  101. package/dist-pkg/render/cvd-matrices.d.ts +5 -1
  102. package/dist-pkg/render/cvd-matrices.js +5 -1
  103. package/dist-pkg/render/high-contrast.d.ts +1 -2
  104. package/dist-pkg/render/high-contrast.js +5 -1
  105. package/dist-pkg/render/viz-axes.d.ts +9 -0
  106. package/dist-pkg/render/viz-axes.js +15 -2
  107. package/dist-pkg/ui/aac-sets.d.ts +29 -24
  108. package/dist-pkg/ui/aac-sets.js +22 -30
  109. package/dist-pkg/ui/audio-choices.d.ts +10 -5
  110. package/dist-pkg/ui/audio-choices.js +14 -10
  111. package/dist-pkg/ui/camera-control.d.ts +47 -0
  112. package/dist-pkg/ui/camera-control.js +35 -1
  113. package/dist-pkg/ui/declared-words.d.ts +21 -0
  114. package/dist-pkg/ui/declared-words.js +66 -0
  115. package/dist-pkg/ui/drawing-problems.js +4 -3
  116. package/dist-pkg/ui/eye-control.js +17 -17
  117. package/dist-pkg/ui/face-control.js +15 -15
  118. package/dist-pkg/ui/fonts.d.ts +42 -8
  119. package/dist-pkg/ui/fonts.js +58 -24
  120. package/dist-pkg/ui/footer-scroll-driver.d.ts +14 -0
  121. package/dist-pkg/ui/footer-scroll-driver.js +201 -0
  122. package/dist-pkg/ui/footer-scroll.d.ts +38 -0
  123. package/dist-pkg/ui/footer-scroll.js +55 -0
  124. package/dist-pkg/ui/game-options.d.ts +14 -9
  125. package/dist-pkg/ui/game-options.js +41 -21
  126. package/dist-pkg/ui/hand-control.js +15 -15
  127. package/dist-pkg/ui/help-panel.d.ts +19 -8
  128. package/dist-pkg/ui/help-panel.js +15 -5
  129. package/dist-pkg/ui/hud-bands.d.ts +29 -12
  130. package/dist-pkg/ui/hud-bands.js +63 -25
  131. package/dist-pkg/ui/hud-row.d.ts +34 -0
  132. package/dist-pkg/ui/hud-row.js +80 -0
  133. package/dist-pkg/ui/hud.d.ts +0 -3
  134. package/dist-pkg/ui/layout.js +5 -2
  135. package/dist-pkg/ui/libras-avatar-clip.d.ts +30 -0
  136. package/dist-pkg/ui/libras-avatar-clip.js +86 -0
  137. package/dist-pkg/ui/libras-avatar-load.d.ts +52 -0
  138. package/dist-pkg/ui/libras-avatar-load.js +134 -0
  139. package/dist-pkg/ui/libras-avatar-plan.d.ts +102 -0
  140. package/dist-pkg/ui/libras-avatar-plan.js +211 -0
  141. package/dist-pkg/ui/libras-avatar-player.d.ts +28 -0
  142. package/dist-pkg/ui/libras-avatar-player.js +226 -0
  143. package/dist-pkg/ui/libras-avatar-stage.d.ts +37 -0
  144. package/dist-pkg/ui/libras-avatar-stage.js +208 -0
  145. package/dist-pkg/ui/libras-glosses.d.ts +28 -0
  146. package/dist-pkg/ui/libras-glosses.js +161 -0
  147. package/dist-pkg/ui/menu-intent.d.ts +29 -0
  148. package/dist-pkg/ui/menu-intent.js +35 -1
  149. package/dist-pkg/ui/menu-items.d.ts +10 -0
  150. package/dist-pkg/ui/menu-items.js +29 -0
  151. package/dist-pkg/ui/menu-nav.d.ts +40 -4
  152. package/dist-pkg/ui/menu-nav.js +132 -47
  153. package/dist-pkg/ui/panel-widgets.d.ts +34 -0
  154. package/dist-pkg/ui/panel-widgets.js +86 -0
  155. package/dist-pkg/ui/pause-icons.d.ts +30 -14
  156. package/dist-pkg/ui/pause-icons.js +106 -45
  157. package/dist-pkg/ui/scan-overlay.d.ts +3 -3
  158. package/dist-pkg/ui/scan-overlay.js +11 -3
  159. package/dist-pkg/ui/screen-text.d.ts +25 -0
  160. package/dist-pkg/ui/screen-text.js +152 -0
  161. package/dist-pkg/ui/session-clock.d.ts +32 -0
  162. package/dist-pkg/ui/session-clock.js +67 -0
  163. package/dist-pkg/ui/settings-aac.d.ts +3 -1
  164. package/dist-pkg/ui/settings-aac.js +32 -13
  165. package/dist-pkg/ui/settings-audio.d.ts +10 -5
  166. package/dist-pkg/ui/settings-audio.js +101 -35
  167. package/dist-pkg/ui/settings-controls.d.ts +0 -4
  168. package/dist-pkg/ui/settings-mobility.d.ts +3 -6
  169. package/dist-pkg/ui/settings-mobility.js +4 -46
  170. package/dist-pkg/ui/settings-motion.js +2 -2
  171. package/dist-pkg/ui/settings-panel.d.ts +10 -0
  172. package/dist-pkg/ui/settings-panel.js +25 -1
  173. package/dist-pkg/ui/settings-typo.d.ts +5 -6
  174. package/dist-pkg/ui/settings-typo.js +20 -11
  175. package/dist-pkg/ui/settings-visual.js +2 -2
  176. package/dist-pkg/ui/shell.d.ts +1 -3
  177. package/dist-pkg/ui/shell.js +8 -4
  178. package/dist-pkg/ui/title.d.ts +2 -11
  179. package/dist-pkg/ui/title.js +39 -6
  180. package/dist-pkg/ui/top-band.d.ts +5 -3
  181. package/dist-pkg/ui/top-band.js +21 -25
  182. package/dist-pkg/ui/typo-choices.d.ts +3 -3
  183. package/dist-pkg/ui/typo-choices.js +9 -8
  184. package/dist-pkg/ui/vlibras.d.ts +71 -27
  185. package/dist-pkg/ui/vlibras.js +116 -44
  186. package/dist-pkg/ui/voice-control.d.ts +23 -1
  187. package/dist-pkg/ui/voice-control.js +149 -16
  188. package/dist-pkg/ui/voice-settings.d.ts +2 -3
  189. package/dist-pkg/ui/voice-settings.js +53 -3
  190. package/dist-pkg/ui/where-the-child-is.js +2 -2
  191. package/docs/CREDITS.md +122 -10
  192. package/docs/LICENSES.md +36 -7
  193. package/package.json +34 -9
  194. package/scripts/check-cartridge.mjs +78 -0
  195. package/scripts/game-build.d.mts +25 -0
  196. package/scripts/game-build.mjs +144 -0
  197. package/scripts/heavy-into-the-delivery.mjs +284 -40
  198. package/scripts/libras-avatar.json +680 -0
  199. package/scripts/libras-avatar.mjs +246 -0
  200. package/scripts/libras-glosses/gloss.py +95 -0
  201. package/scripts/libras-glosses/pyproject.toml +32 -0
  202. package/scripts/libras-glosses/uv.lock +781 -0
  203. package/scripts/libras-glosses.mjs +351 -0
  204. package/scripts/licences/OFL-1.1.txt +86 -0
  205. package/scripts/licences/UFL-1.0.txt +96 -0
  206. package/scripts/licences/fonts.mjs +389 -0
  207. package/scripts/licences/third-party.mjs +35 -21
  208. package/app/public/vendor/fonts/bitter-var-ext.woff2 +0 -0
  209. package/app/public/vendor/fonts/bitter-var.woff2 +0 -0
  210. package/app/public/vendor/fonts/bodonimoda-var-ext.woff2 +0 -0
  211. package/app/public/vendor/fonts/bodonimoda-var.woff2 +0 -0
  212. package/app/public/vendor/fonts/cat-abril_fatface-400-ext.woff2 +0 -0
  213. package/app/public/vendor/fonts/cat-abril_fatface-400.woff2 +0 -0
  214. package/app/public/vendor/fonts/cat-adlam_display-400-ext.woff2 +0 -0
  215. package/app/public/vendor/fonts/cat-adlam_display-400.woff2 +0 -0
  216. package/app/public/vendor/fonts/cat-agu_display-400-ext.woff2 +0 -0
  217. package/app/public/vendor/fonts/cat-agu_display-400.woff2 +0 -0
  218. package/app/public/vendor/fonts/cat-alfa_slab_one-400-ext.woff2 +0 -0
  219. package/app/public/vendor/fonts/cat-alfa_slab_one-400.woff2 +0 -0
  220. package/app/public/vendor/fonts/cat-are_you_serious-400-ext.woff2 +0 -0
  221. package/app/public/vendor/fonts/cat-are_you_serious-400.woff2 +0 -0
  222. package/app/public/vendor/fonts/cat-asimovian-400-ext.woff2 +0 -0
  223. package/app/public/vendor/fonts/cat-asimovian-400.woff2 +0 -0
  224. package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400-ext.woff2 +0 -0
  225. package/app/public/vendor/fonts/cat-atkinson_hyperlegible_next-400.woff2 +0 -0
  226. package/app/public/vendor/fonts/cat-bakbak_one-400-ext.woff2 +0 -0
  227. package/app/public/vendor/fonts/cat-bakbak_one-400.woff2 +0 -0
  228. package/app/public/vendor/fonts/cat-bangers-400-ext.woff2 +0 -0
  229. package/app/public/vendor/fonts/cat-bangers-400.woff2 +0 -0
  230. package/app/public/vendor/fonts/cat-barriecito-400-ext.woff2 +0 -0
  231. package/app/public/vendor/fonts/cat-barriecito-400.woff2 +0 -0
  232. package/app/public/vendor/fonts/cat-berkshire_swash-400-ext.woff2 +0 -0
  233. package/app/public/vendor/fonts/cat-berkshire_swash-400.woff2 +0 -0
  234. package/app/public/vendor/fonts/cat-bevan-400-ext.woff2 +0 -0
  235. package/app/public/vendor/fonts/cat-bevan-400.woff2 +0 -0
  236. package/app/public/vendor/fonts/cat-bitcount-400-ext.woff2 +0 -0
  237. package/app/public/vendor/fonts/cat-bitcount-400.woff2 +0 -0
  238. package/app/public/vendor/fonts/cat-bowlby_one_sc-400-ext.woff2 +0 -0
  239. package/app/public/vendor/fonts/cat-bowlby_one_sc-400.woff2 +0 -0
  240. package/app/public/vendor/fonts/cat-bungee_shade-400-ext.woff2 +0 -0
  241. package/app/public/vendor/fonts/cat-bungee_shade-400.woff2 +0 -0
  242. package/app/public/vendor/fonts/cat-charm-400-ext.woff2 +0 -0
  243. package/app/public/vendor/fonts/cat-charm-400.woff2 +0 -0
  244. package/app/public/vendor/fonts/cat-charm-700-ext.woff2 +0 -0
  245. package/app/public/vendor/fonts/cat-charm-700.woff2 +0 -0
  246. package/app/public/vendor/fonts/cat-cherry_bomb_one-400-ext.woff2 +0 -0
  247. package/app/public/vendor/fonts/cat-cherry_bomb_one-400.woff2 +0 -0
  248. package/app/public/vendor/fonts/cat-chokokutai-400-ext.woff2 +0 -0
  249. package/app/public/vendor/fonts/cat-chokokutai-400.woff2 +0 -0
  250. package/app/public/vendor/fonts/cat-chonburi-400-ext.woff2 +0 -0
  251. package/app/public/vendor/fonts/cat-chonburi-400.woff2 +0 -0
  252. package/app/public/vendor/fonts/cat-climate_crisis-400-ext.woff2 +0 -0
  253. package/app/public/vendor/fonts/cat-climate_crisis-400.woff2 +0 -0
  254. package/app/public/vendor/fonts/cat-cookie-400.woff2 +0 -0
  255. package/app/public/vendor/fonts/cat-dela_gothic_one-400-ext.woff2 +0 -0
  256. package/app/public/vendor/fonts/cat-dela_gothic_one-400.woff2 +0 -0
  257. package/app/public/vendor/fonts/cat-fascinate_inline-400-ext.woff2 +0 -0
  258. package/app/public/vendor/fonts/cat-fascinate_inline-400.woff2 +0 -0
  259. package/app/public/vendor/fonts/cat-ga_maamli-400-ext.woff2 +0 -0
  260. package/app/public/vendor/fonts/cat-ga_maamli-400.woff2 +0 -0
  261. package/app/public/vendor/fonts/cat-itim-400-ext.woff2 +0 -0
  262. package/app/public/vendor/fonts/cat-itim-400.woff2 +0 -0
  263. package/app/public/vendor/fonts/cat-jacquard_24-400-ext.woff2 +0 -0
  264. package/app/public/vendor/fonts/cat-jacquard_24-400.woff2 +0 -0
  265. package/app/public/vendor/fonts/cat-kablammo-400-ext.woff2 +0 -0
  266. package/app/public/vendor/fonts/cat-kablammo-400.woff2 +0 -0
  267. package/app/public/vendor/fonts/cat-kalam-400-ext.woff2 +0 -0
  268. package/app/public/vendor/fonts/cat-kalam-400.woff2 +0 -0
  269. package/app/public/vendor/fonts/cat-kalam-700-ext.woff2 +0 -0
  270. package/app/public/vendor/fonts/cat-kalam-700.woff2 +0 -0
  271. package/app/public/vendor/fonts/cat-katibeh-400-ext.woff2 +0 -0
  272. package/app/public/vendor/fonts/cat-katibeh-400.woff2 +0 -0
  273. package/app/public/vendor/fonts/cat-lemonada-400-ext.woff2 +0 -0
  274. package/app/public/vendor/fonts/cat-lemonada-400.woff2 +0 -0
  275. package/app/public/vendor/fonts/cat-lilita_one-400-ext.woff2 +0 -0
  276. package/app/public/vendor/fonts/cat-lilita_one-400.woff2 +0 -0
  277. package/app/public/vendor/fonts/cat-lobster-400-ext.woff2 +0 -0
  278. package/app/public/vendor/fonts/cat-lobster-400.woff2 +0 -0
  279. package/app/public/vendor/fonts/cat-luckiest_guy-400-ext.woff2 +0 -0
  280. package/app/public/vendor/fonts/cat-luckiest_guy-400.woff2 +0 -0
  281. package/app/public/vendor/fonts/cat-marhey-400-ext.woff2 +0 -0
  282. package/app/public/vendor/fonts/cat-marhey-400.woff2 +0 -0
  283. package/app/public/vendor/fonts/cat-modak-400-ext.woff2 +0 -0
  284. package/app/public/vendor/fonts/cat-modak-400.woff2 +0 -0
  285. package/app/public/vendor/fonts/cat-momo_signature-400-ext.woff2 +0 -0
  286. package/app/public/vendor/fonts/cat-momo_signature-400.woff2 +0 -0
  287. package/app/public/vendor/fonts/cat-monofett-400-ext.woff2 +0 -0
  288. package/app/public/vendor/fonts/cat-monofett-400.woff2 +0 -0
  289. package/app/public/vendor/fonts/cat-monomakh-400-ext.woff2 +0 -0
  290. package/app/public/vendor/fonts/cat-monomakh-400.woff2 +0 -0
  291. package/app/public/vendor/fonts/cat-monoton-400-ext.woff2 +0 -0
  292. package/app/public/vendor/fonts/cat-monoton-400.woff2 +0 -0
  293. package/app/public/vendor/fonts/cat-nerko_one-400-ext.woff2 +0 -0
  294. package/app/public/vendor/fonts/cat-nerko_one-400.woff2 +0 -0
  295. package/app/public/vendor/fonts/cat-noto_sans_adlam-400-ext.woff2 +0 -0
  296. package/app/public/vendor/fonts/cat-noto_sans_adlam-400.woff2 +0 -0
  297. package/app/public/vendor/fonts/cat-noto_sans_arabic-400-ext.woff2 +0 -0
  298. package/app/public/vendor/fonts/cat-noto_sans_arabic-400.woff2 +0 -0
  299. package/app/public/vendor/fonts/cat-noto_sans_devanagari-400-ext.woff2 +0 -0
  300. package/app/public/vendor/fonts/cat-noto_sans_devanagari-400.woff2 +0 -0
  301. package/app/public/vendor/fonts/cat-noto_sans_hebrew-400-ext.woff2 +0 -0
  302. package/app/public/vendor/fonts/cat-noto_sans_hebrew-400.woff2 +0 -0
  303. package/app/public/vendor/fonts/cat-noto_sans_jp-400-ext.woff2 +0 -0
  304. package/app/public/vendor/fonts/cat-noto_sans_jp-400.woff2 +0 -0
  305. package/app/public/vendor/fonts/cat-noto_sans_khmer-400-ext.woff2 +0 -0
  306. package/app/public/vendor/fonts/cat-noto_sans_khmer-400.woff2 +0 -0
  307. package/app/public/vendor/fonts/cat-noto_sans_kr-400-ext.woff2 +0 -0
  308. package/app/public/vendor/fonts/cat-noto_sans_kr-400.woff2 +0 -0
  309. package/app/public/vendor/fonts/cat-noto_sans_sc-400-ext.woff2 +0 -0
  310. package/app/public/vendor/fonts/cat-noto_sans_sc-400.woff2 +0 -0
  311. package/app/public/vendor/fonts/cat-noto_sans_thai-400-ext.woff2 +0 -0
  312. package/app/public/vendor/fonts/cat-noto_sans_thai-400.woff2 +0 -0
  313. package/app/public/vendor/fonts/cat-ole-400-ext.woff2 +0 -0
  314. package/app/public/vendor/fonts/cat-ole-400.woff2 +0 -0
  315. package/app/public/vendor/fonts/cat-pacifico-400-ext.woff2 +0 -0
  316. package/app/public/vendor/fonts/cat-pacifico-400.woff2 +0 -0
  317. package/app/public/vendor/fonts/cat-patrick_hand-400-ext.woff2 +0 -0
  318. package/app/public/vendor/fonts/cat-patrick_hand-400.woff2 +0 -0
  319. package/app/public/vendor/fonts/cat-petit_formal_script-400-ext.woff2 +0 -0
  320. package/app/public/vendor/fonts/cat-petit_formal_script-400.woff2 +0 -0
  321. package/app/public/vendor/fonts/cat-pixelify_sans-400-ext.woff2 +0 -0
  322. package/app/public/vendor/fonts/cat-pixelify_sans-400.woff2 +0 -0
  323. package/app/public/vendor/fonts/cat-plaster-400-ext.woff2 +0 -0
  324. package/app/public/vendor/fonts/cat-plaster-400.woff2 +0 -0
  325. package/app/public/vendor/fonts/cat-playwrite_ar_guides-400-0.woff2 +0 -0
  326. package/app/public/vendor/fonts/cat-playwrite_at-400-0.woff2 +0 -0
  327. package/app/public/vendor/fonts/cat-playwrite_at_guides-400-0.woff2 +0 -0
  328. package/app/public/vendor/fonts/cat-playwrite_au_nsw-400-0.woff2 +0 -0
  329. package/app/public/vendor/fonts/cat-playwrite_au_nsw_guides-400-0.woff2 +0 -0
  330. package/app/public/vendor/fonts/cat-playwrite_au_qld-400-0.woff2 +0 -0
  331. package/app/public/vendor/fonts/cat-playwrite_au_qld_guides-400-0.woff2 +0 -0
  332. package/app/public/vendor/fonts/cat-playwrite_au_sa-400-0.woff2 +0 -0
  333. package/app/public/vendor/fonts/cat-playwrite_au_sa_guides-400-0.woff2 +0 -0
  334. package/app/public/vendor/fonts/cat-playwrite_au_tas-400-0.woff2 +0 -0
  335. package/app/public/vendor/fonts/cat-playwrite_au_tas_guides-400-0.woff2 +0 -0
  336. package/app/public/vendor/fonts/cat-playwrite_au_vic-400-0.woff2 +0 -0
  337. package/app/public/vendor/fonts/cat-playwrite_au_vic_guides-400-0.woff2 +0 -0
  338. package/app/public/vendor/fonts/cat-playwrite_be_vlg-400-0.woff2 +0 -0
  339. package/app/public/vendor/fonts/cat-playwrite_be_vlg_guides-400-0.woff2 +0 -0
  340. package/app/public/vendor/fonts/cat-playwrite_be_wal-400-0.woff2 +0 -0
  341. package/app/public/vendor/fonts/cat-playwrite_be_wal_guides-400-0.woff2 +0 -0
  342. package/app/public/vendor/fonts/cat-playwrite_br_guides-400-0.woff2 +0 -0
  343. package/app/public/vendor/fonts/cat-playwrite_ca_guides-400-0.woff2 +0 -0
  344. package/app/public/vendor/fonts/cat-playwrite_cl_guides-400-0.woff2 +0 -0
  345. package/app/public/vendor/fonts/cat-playwrite_co_guides-400-0.woff2 +0 -0
  346. package/app/public/vendor/fonts/cat-playwrite_cu_guides-400-0.woff2 +0 -0
  347. package/app/public/vendor/fonts/cat-playwrite_cz-400-0.woff2 +0 -0
  348. package/app/public/vendor/fonts/cat-playwrite_cz_guides-400-0.woff2 +0 -0
  349. package/app/public/vendor/fonts/cat-playwrite_de_grund-400-0.woff2 +0 -0
  350. package/app/public/vendor/fonts/cat-playwrite_de_grund_guides-400-0.woff2 +0 -0
  351. package/app/public/vendor/fonts/cat-playwrite_de_la-400-0.woff2 +0 -0
  352. package/app/public/vendor/fonts/cat-playwrite_de_la_guides-400-0.woff2 +0 -0
  353. package/app/public/vendor/fonts/cat-playwrite_de_sas-400-0.woff2 +0 -0
  354. package/app/public/vendor/fonts/cat-playwrite_de_sas_guides-400-0.woff2 +0 -0
  355. package/app/public/vendor/fonts/cat-playwrite_de_va-400-0.woff2 +0 -0
  356. package/app/public/vendor/fonts/cat-playwrite_de_va_guides-400-0.woff2 +0 -0
  357. package/app/public/vendor/fonts/cat-playwrite_dk_loopet-400-0.woff2 +0 -0
  358. package/app/public/vendor/fonts/cat-playwrite_dk_loopet_guides-400-0.woff2 +0 -0
  359. package/app/public/vendor/fonts/cat-playwrite_dk_uloopet-400-0.woff2 +0 -0
  360. package/app/public/vendor/fonts/cat-playwrite_dk_uloopet_guides-400-0.woff2 +0 -0
  361. package/app/public/vendor/fonts/cat-playwrite_es_deco_guides-400-0.woff2 +0 -0
  362. package/app/public/vendor/fonts/cat-playwrite_es_guides-400-0.woff2 +0 -0
  363. package/app/public/vendor/fonts/cat-playwrite_fr_moderne-400-0.woff2 +0 -0
  364. package/app/public/vendor/fonts/cat-playwrite_fr_moderne_guides-400-0.woff2 +0 -0
  365. package/app/public/vendor/fonts/cat-playwrite_fr_trad-400-0.woff2 +0 -0
  366. package/app/public/vendor/fonts/cat-playwrite_fr_trad_guides-400-0.woff2 +0 -0
  367. package/app/public/vendor/fonts/cat-playwrite_gb_j_guides-400-0.woff2 +0 -0
  368. package/app/public/vendor/fonts/cat-playwrite_gb_s_guides-400-0.woff2 +0 -0
  369. package/app/public/vendor/fonts/cat-playwrite_hr-400-0.woff2 +0 -0
  370. package/app/public/vendor/fonts/cat-playwrite_hr_guides-400-0.woff2 +0 -0
  371. package/app/public/vendor/fonts/cat-playwrite_hr_lijeva-400-0.woff2 +0 -0
  372. package/app/public/vendor/fonts/cat-playwrite_hr_lijeva_guides-400-0.woff2 +0 -0
  373. package/app/public/vendor/fonts/cat-playwrite_hu-400-0.woff2 +0 -0
  374. package/app/public/vendor/fonts/cat-playwrite_hu_guides-400-0.woff2 +0 -0
  375. package/app/public/vendor/fonts/cat-playwrite_id-400-0.woff2 +0 -0
  376. package/app/public/vendor/fonts/cat-playwrite_id_guides-400-0.woff2 +0 -0
  377. package/app/public/vendor/fonts/cat-playwrite_ie-400-0.woff2 +0 -0
  378. package/app/public/vendor/fonts/cat-playwrite_ie_guides-400-0.woff2 +0 -0
  379. package/app/public/vendor/fonts/cat-playwrite_in-400-0.woff2 +0 -0
  380. package/app/public/vendor/fonts/cat-playwrite_in_guides-400-0.woff2 +0 -0
  381. package/app/public/vendor/fonts/cat-playwrite_is-400-0.woff2 +0 -0
  382. package/app/public/vendor/fonts/cat-playwrite_is_guides-400-0.woff2 +0 -0
  383. package/app/public/vendor/fonts/cat-playwrite_it_moderna-400-0.woff2 +0 -0
  384. package/app/public/vendor/fonts/cat-playwrite_it_moderna_guides-400-0.woff2 +0 -0
  385. package/app/public/vendor/fonts/cat-playwrite_it_trad-400-0.woff2 +0 -0
  386. package/app/public/vendor/fonts/cat-playwrite_it_trad_guides-400-0.woff2 +0 -0
  387. package/app/public/vendor/fonts/cat-playwrite_mx_guides-400-0.woff2 +0 -0
  388. package/app/public/vendor/fonts/cat-playwrite_ng_modern-400-0.woff2 +0 -0
  389. package/app/public/vendor/fonts/cat-playwrite_ng_modern_guides-400-0.woff2 +0 -0
  390. package/app/public/vendor/fonts/cat-playwrite_nl-400-0.woff2 +0 -0
  391. package/app/public/vendor/fonts/cat-playwrite_nl_guides-400-0.woff2 +0 -0
  392. package/app/public/vendor/fonts/cat-playwrite_no-400-0.woff2 +0 -0
  393. package/app/public/vendor/fonts/cat-playwrite_no_guides-400-0.woff2 +0 -0
  394. package/app/public/vendor/fonts/cat-playwrite_nz-400-0.woff2 +0 -0
  395. package/app/public/vendor/fonts/cat-playwrite_nz_basic-400-0.woff2 +0 -0
  396. package/app/public/vendor/fonts/cat-playwrite_nz_basic_guides-400-0.woff2 +0 -0
  397. package/app/public/vendor/fonts/cat-playwrite_nz_guides-400-0.woff2 +0 -0
  398. package/app/public/vendor/fonts/cat-playwrite_pe_guides-400-0.woff2 +0 -0
  399. package/app/public/vendor/fonts/cat-playwrite_pl-400-0.woff2 +0 -0
  400. package/app/public/vendor/fonts/cat-playwrite_pl_guides-400-0.woff2 +0 -0
  401. package/app/public/vendor/fonts/cat-playwrite_pt_guides-400-0.woff2 +0 -0
  402. package/app/public/vendor/fonts/cat-playwrite_ro-400-0.woff2 +0 -0
  403. package/app/public/vendor/fonts/cat-playwrite_ro_guides-400-0.woff2 +0 -0
  404. package/app/public/vendor/fonts/cat-playwrite_sk-400-0.woff2 +0 -0
  405. package/app/public/vendor/fonts/cat-playwrite_sk_guides-400-0.woff2 +0 -0
  406. package/app/public/vendor/fonts/cat-playwrite_tz-400-0.woff2 +0 -0
  407. package/app/public/vendor/fonts/cat-playwrite_tz_guides-400-0.woff2 +0 -0
  408. package/app/public/vendor/fonts/cat-playwrite_us_modern_guides-400-0.woff2 +0 -0
  409. package/app/public/vendor/fonts/cat-playwrite_us_trad_guides-400-0.woff2 +0 -0
  410. package/app/public/vendor/fonts/cat-playwrite_vn-400-0.woff2 +0 -0
  411. package/app/public/vendor/fonts/cat-playwrite_vn_guides-400-0.woff2 +0 -0
  412. package/app/public/vendor/fonts/cat-playwrite_za-400-0.woff2 +0 -0
  413. package/app/public/vendor/fonts/cat-playwrite_za_guides-400-0.woff2 +0 -0
  414. package/app/public/vendor/fonts/cat-potta_one-400-ext.woff2 +0 -0
  415. package/app/public/vendor/fonts/cat-potta_one-400.woff2 +0 -0
  416. package/app/public/vendor/fonts/cat-ranchers-400-ext.woff2 +0 -0
  417. package/app/public/vendor/fonts/cat-ranchers-400.woff2 +0 -0
  418. package/app/public/vendor/fonts/cat-ranga-400-ext.woff2 +0 -0
  419. package/app/public/vendor/fonts/cat-ranga-400.woff2 +0 -0
  420. package/app/public/vendor/fonts/cat-ranga-700-ext.woff2 +0 -0
  421. package/app/public/vendor/fonts/cat-ranga-700.woff2 +0 -0
  422. package/app/public/vendor/fonts/cat-risque-400-ext.woff2 +0 -0
  423. package/app/public/vendor/fonts/cat-risque-400.woff2 +0 -0
  424. package/app/public/vendor/fonts/cat-rowdies-400-ext.woff2 +0 -0
  425. package/app/public/vendor/fonts/cat-rowdies-400.woff2 +0 -0
  426. package/app/public/vendor/fonts/cat-rowdies-700-ext.woff2 +0 -0
  427. package/app/public/vendor/fonts/cat-rowdies-700.woff2 +0 -0
  428. package/app/public/vendor/fonts/cat-ruge_boogie-400-ext.woff2 +0 -0
  429. package/app/public/vendor/fonts/cat-ruge_boogie-400.woff2 +0 -0
  430. package/app/public/vendor/fonts/cat-ruslan_display-400-ext.woff2 +0 -0
  431. package/app/public/vendor/fonts/cat-ruslan_display-400.woff2 +0 -0
  432. package/app/public/vendor/fonts/cat-sacramento-400-ext.woff2 +0 -0
  433. package/app/public/vendor/fonts/cat-sacramento-400.woff2 +0 -0
  434. package/app/public/vendor/fonts/cat-sancreek-400-ext.woff2 +0 -0
  435. package/app/public/vendor/fonts/cat-sancreek-400.woff2 +0 -0
  436. package/app/public/vendor/fonts/cat-sankofa_display-400-ext.woff2 +0 -0
  437. package/app/public/vendor/fonts/cat-sankofa_display-400.woff2 +0 -0
  438. package/app/public/vendor/fonts/cat-sedgwick_ave_display-400-ext.woff2 +0 -0
  439. package/app/public/vendor/fonts/cat-sedgwick_ave_display-400.woff2 +0 -0
  440. package/app/public/vendor/fonts/cat-shantell_sans-400-ext.woff2 +0 -0
  441. package/app/public/vendor/fonts/cat-shantell_sans-400.woff2 +0 -0
  442. package/app/public/vendor/fonts/cat-smokum-400-ext.woff2 +0 -0
  443. package/app/public/vendor/fonts/cat-smokum-400.woff2 +0 -0
  444. package/app/public/vendor/fonts/cat-solitreo-400-ext.woff2 +0 -0
  445. package/app/public/vendor/fonts/cat-solitreo-400.woff2 +0 -0
  446. package/app/public/vendor/fonts/cat-spicy_rice-400-ext.woff2 +0 -0
  447. package/app/public/vendor/fonts/cat-spicy_rice-400.woff2 +0 -0
  448. package/app/public/vendor/fonts/cat-sriracha-400-ext.woff2 +0 -0
  449. package/app/public/vendor/fonts/cat-sriracha-400.woff2 +0 -0
  450. package/app/public/vendor/fonts/cat-srisakdi-400-ext.woff2 +0 -0
  451. package/app/public/vendor/fonts/cat-srisakdi-400.woff2 +0 -0
  452. package/app/public/vendor/fonts/cat-srisakdi-700-ext.woff2 +0 -0
  453. package/app/public/vendor/fonts/cat-srisakdi-700.woff2 +0 -0
  454. package/app/public/vendor/fonts/cat-stix_two_math-400-0.woff2 +0 -0
  455. package/app/public/vendor/fonts/cat-tillana-400-ext.woff2 +0 -0
  456. package/app/public/vendor/fonts/cat-tillana-400.woff2 +0 -0
  457. package/app/public/vendor/fonts/cat-tillana-700-ext.woff2 +0 -0
  458. package/app/public/vendor/fonts/cat-tillana-700.woff2 +0 -0
  459. package/app/public/vendor/fonts/cat-titan_one-400-ext.woff2 +0 -0
  460. package/app/public/vendor/fonts/cat-titan_one-400.woff2 +0 -0
  461. package/app/public/vendor/fonts/cat-yatra_one-400-ext.woff2 +0 -0
  462. package/app/public/vendor/fonts/cat-yatra_one-400.woff2 +0 -0
  463. package/app/public/vendor/fonts/cat-yomogi-400-ext.woff2 +0 -0
  464. package/app/public/vendor/fonts/cat-yomogi-400.woff2 +0 -0
  465. package/app/public/vendor/fonts/dmserifdisplay-400-ext.woff2 +0 -0
  466. package/app/public/vendor/fonts/dmserifdisplay-400.woff2 +0 -0
  467. package/app/public/vendor/fonts/domine-var-ext.woff2 +0 -0
  468. package/app/public/vendor/fonts/domine-var.woff2 +0 -0
  469. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  470. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  471. package/app/public/vendor/fonts/fraunces-var-ext.woff2 +0 -0
  472. package/app/public/vendor/fonts/fraunces-var.woff2 +0 -0
  473. package/app/public/vendor/fonts/fredoka-var-ext.woff2 +0 -0
  474. package/app/public/vendor/fonts/fredoka-var.woff2 +0 -0
  475. package/app/public/vendor/fonts/inter-400.woff2 +0 -0
  476. package/app/public/vendor/fonts/inter-700.woff2 +0 -0
  477. package/app/public/vendor/fonts/jakarta-var-ext.woff2 +0 -0
  478. package/app/public/vendor/fonts/jakarta-var.woff2 +0 -0
  479. package/app/public/vendor/fonts/lato-400.woff2 +0 -0
  480. package/app/public/vendor/fonts/lato-700.woff2 +0 -0
  481. package/app/public/vendor/fonts/literata-400.woff2 +0 -0
  482. package/app/public/vendor/fonts/literata-700.woff2 +0 -0
  483. package/app/public/vendor/fonts/lora-var-ext.woff2 +0 -0
  484. package/app/public/vendor/fonts/lora-var.woff2 +0 -0
  485. package/app/public/vendor/fonts/merriweather-var-ext.woff2 +0 -0
  486. package/app/public/vendor/fonts/merriweather-var.woff2 +0 -0
  487. package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
  488. package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
  489. package/app/public/vendor/fonts/notosans-var-ext.woff2 +0 -0
  490. package/app/public/vendor/fonts/notosans-var.woff2 +0 -0
  491. package/app/public/vendor/fonts/nunito-var-ext.woff2 +0 -0
  492. package/app/public/vendor/fonts/nunito-var.woff2 +0 -0
  493. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  494. package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
  495. package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
  496. package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
  497. package/app/public/vendor/fonts/playfair-var-ext.woff2 +0 -0
  498. package/app/public/vendor/fonts/playfair-var.woff2 +0 -0
  499. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  500. package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
  501. package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
  502. package/app/public/vendor/fonts/quicksand-var-ext.woff2 +0 -0
  503. package/app/public/vendor/fonts/quicksand-var.woff2 +0 -0
  504. package/app/public/vendor/fonts/robotoflex-var-ext.woff2 +0 -0
  505. package/app/public/vendor/fonts/robotoflex-var.woff2 +0 -0
  506. package/app/public/vendor/fonts/sora-var-ext.woff2 +0 -0
  507. package/app/public/vendor/fonts/sora-var.woff2 +0 -0
  508. package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
  509. package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
  510. package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
  511. package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
  512. package/app/public/vendor/fonts/spacegrotesk-var-ext.woff2 +0 -0
  513. package/app/public/vendor/fonts/spacegrotesk-var.woff2 +0 -0
  514. package/app/public/vendor/fonts/spectral-400-ext.woff2 +0 -0
  515. package/app/public/vendor/fonts/spectral-400.woff2 +0 -0
  516. package/app/public/vendor/fonts/spectral-700-ext.woff2 +0 -0
  517. package/app/public/vendor/fonts/spectral-700.woff2 +0 -0
  518. package/app/public/vendor/fonts/teachers-var-ext.woff2 +0 -0
  519. package/app/public/vendor/fonts/teachers-var.woff2 +0 -0
  520. package/app/public/vendor/fonts/ubuntu-400-ext.woff2 +0 -0
  521. package/app/public/vendor/fonts/ubuntu-400.woff2 +0 -0
  522. package/app/public/vendor/fonts/ubuntu-700-ext.woff2 +0 -0
  523. package/app/public/vendor/fonts/ubuntu-700.woff2 +0 -0
  524. package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
  525. package/dist-pkg/platform/guide-intensity.d.ts +0 -39
  526. package/dist-pkg/platform/guide-intensity.js +0 -71
  527. package/dist-pkg/ui/latch-refusal.d.ts +0 -33
  528. package/dist-pkg/ui/latch-refusal.js +0 -60
@@ -31,6 +31,7 @@
31
31
  // in milliseconds.
32
32
  export function createInterruptibleSpeech(engine) {
33
33
  let nowPlaying = null;
34
+ let nowFinished = null;
34
35
  let currentTurn = 0;
35
36
  /** Silences whatever is playing. The `try` exists because stopping an already finished source throws in some engines. */
36
37
  function stopPlayback() {
@@ -41,37 +42,60 @@ export function createInterruptibleSpeech(engine) {
41
42
  catch (e) { /* the source already ended — stopping again is no error */ }
42
43
  nowPlaying = null;
43
44
  }
45
+ const finished = nowFinished;
46
+ nowFinished = null;
47
+ finished?.();
44
48
  }
45
- async function speakNow(text) {
49
+ async function speakNow(request, finished) {
46
50
  const myTurn = ++currentTurn; // claims the turn BEFORE any wait
47
51
  stopPlayback(); // guarantee 1: immediate silence, not at the end of synthesis
48
- if (!text)
52
+ if (!request) {
53
+ finished();
49
54
  return;
55
+ }
50
56
  try {
51
- const audio = await engine.synthesize(text);
52
- if (myTurn !== currentTurn)
53
- return; // guarantee 2: it arrived late — another request already took over
57
+ const audio = await engine.synthesize(request);
58
+ if (myTurn !== currentTurn) {
59
+ finished();
60
+ return;
61
+ } // guarantee 2: it arrived late — another request already took over
54
62
  stopPlayback(); // again: something may have started playing during the wait
55
- const playing = engine.play(audio, () => { if (nowPlaying === playing)
56
- nowPlaying = null; });
63
+ const playing = engine.play(audio, () => {
64
+ if (nowPlaying === playing) {
65
+ nowPlaying = null;
66
+ nowFinished = null;
67
+ }
68
+ finished();
69
+ });
70
+ if (playing === null) {
71
+ finished();
72
+ return;
73
+ }
57
74
  if (myTurn !== currentTurn) {
58
- if (playing !== null) {
59
- try {
60
- engine.stop(playing);
61
- }
62
- catch (e) { /* noop */ }
75
+ try {
76
+ engine.stop(playing);
63
77
  }
78
+ catch (e) { /* noop */ }
79
+ finished();
64
80
  return;
65
81
  }
66
82
  nowPlaying = playing;
83
+ nowFinished = finished;
67
84
  }
68
85
  catch (e) {
69
86
  // Synthesis failed for THIS text. No reason to bring the whole narration down: the next request tries again, and one
70
87
  // silent item is better than a dead engine.
88
+ finished();
71
89
  }
72
90
  }
91
+ /** Calls `finished` at most once, whichever of the ends above comes first. */
92
+ const once = (finished) => {
93
+ let called = false;
94
+ return () => { if (called)
95
+ return; called = true; finished?.(); };
96
+ };
73
97
  return {
74
- speak: (text) => { void speakNow(text); },
98
+ speak: (request, finished) => { void speakNow(request, once(finished)); },
75
99
  silence: () => { currentTurn++; stopPlayback(); }, // the `++` invalidates whatever is being synthesised now
76
100
  speaking: () => nowPlaying !== null,
77
101
  };
@@ -46,18 +46,29 @@ export function createListenerScope(real) {
46
46
  * ⚠️ WHAT LEAVES THIS TRAP IS ANSWERED BY THE REAL WINDOW, NEVER BY THE PROXY, and it is not style: `innerWidth`,
47
47
  * `devicePixelRatio` and `location` are GETTERS on the window, and `getComputedStyle`, `requestAnimationFrame` and
48
48
  * `matchMedia` are methods of it — run any of them with the proxy as `this` and the browser answers «Illegal invocation».
49
- * So the receiver is left at its default (which IS the target) and every function is bound before it goes out.
49
+ * So the receiver is left at its default (which IS the target) and every function leaves answered by the real window.
50
50
  *
51
- * 📌 Both halves are exercised, by mutation: forwarding the proxy as the receiver kills the whole collection at load, and
52
- * dropping the `bind` kills it at the first `getComputedStyle`.
53
- *
54
- * 🔴 BUT A CONSTRUCTOR LEAVES UNBOUND, and this was not foreseen — the suite found it. `bind` returns a function with NO
55
- * `prototype`, and code that reaches a constructor THROUGH the window reads that prototype: `platform/speech-recognition`
56
- * asks `'processLocally' in api.prototype` before it will use the browser's recogniser on the device, and bound, that read is
57
- * `in undefined`. 📏 Measured: `tests/reading-no-createGame.browser.test.js` went red — the child who reads aloud got no
58
- * microphone at all. A constructor never needs `this` from the property access; a method always does, and `prototype` is the
59
- * one thing that tells the two apart.
51
+ * 🔴 EVERY FUNCTION, WITH NO GUESS AT WHAT IT IS (`onTheWindow`): a call runs with the real window as `this`, and everything
52
+ * else — `new`, the `prototype`, a static like `SpeechRecognition.available` — reaches the function itself. The rule before
53
+ * guessed: a function with a `prototype` was a constructor and left unbound, the rest were `bind`-ed. The guess held for the
54
+ * browser's own functions and failed for an extension's: one that wraps `window.fetch` does it with a plain `function`, which
55
+ * HAS a `prototype`, so the engine's fetch left unbound and every heavy file failed with «Illegal invocation» inside the
56
+ * download's `catch`. 📏 Measured on 2026-09-27 in the Dev's Brave: no heavy file ever requested, an empty checked cache, and
57
+ * the 👄 and the 📷 unable to start; with a wrapped fetch in a clean profile, 0 requests against 66 without it. A `bind` could
58
+ * not be the answer either: it drops the `prototype` that `platform/speech-recognition` reads.
60
59
  */
60
+ const answered = new WeakMap();
61
+ const onTheWindow = (fn) => {
62
+ // one wrapper per function, so the same property read twice is the same function; `new` builds with the function itself as
63
+ // the target — with the wrapper as `newTarget`, a constructor already bound (a host may lend one) built an object with none of
64
+ // its methods («observe is not a function», measured in `tests/no-need-to-hold.browser.test.js`)
65
+ if (!answered.has(fn))
66
+ answered.set(fn, new Proxy(fn, {
67
+ apply: (f, _this, args) => Reflect.apply(f, real, args),
68
+ construct: (f, args) => Reflect.construct(f, args),
69
+ }));
70
+ return answered.get(fn);
71
+ };
61
72
  const win = new Proxy(real, {
62
73
  get(target, prop) {
63
74
  if (prop === 'addEventListener')
@@ -65,9 +76,7 @@ export function createListenerScope(real) {
65
76
  if (prop === 'removeEventListener')
66
77
  return forget;
67
78
  const value = Reflect.get(target, prop);
68
- if (typeof value !== 'function')
69
- return value;
70
- return Object.hasOwn(value, 'prototype') ? value : value.bind(target);
79
+ return typeof value === 'function' ? onTheWindow(value) : value;
71
80
  },
72
81
  });
73
82
  const releaseAll = () => {
@@ -1,10 +1,14 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
- /** The two hooks `core/i18n`'s port takes. A host that has no page passes neither, and the engine runs without one. */
2
+ /** The three hooks `core/i18n`'s port takes. A host that has no page passes none, and the engine runs without one. */
3
3
  export interface LocaleHostHooks {
4
4
  readonly applied: (locale: string, tag: string) => void;
5
5
  readonly preferred: () => string | null;
6
+ readonly follow: (react: (locale: string) => void) => void;
6
7
  }
7
- /** What a browser can offer `core/i18n`: the page effects of a change, and the language the person's browser is set to. */
8
+ /**
9
+ * What a browser can offer a translator: the page effects of a change, the language the person's browser is set to, and
10
+ * the page's signal that ANOTHER root changed the language (ADR-0232 D3: the language is the page's, and every root follows).
11
+ */
8
12
  export declare function localeHostHooks(doc: Document, win: Window, applyDom: (root: ParentNode) => void): LocaleHostHooks;
9
- /** Hangs the i18n object on the window for tests and the preview, which is what `core/i18n` used to do to itself. */
13
+ /** Hangs a root's translator on the window for tests and the preview, which is what `core/i18n` used to do to itself. */
10
14
  export declare function exposeI18n(win: Window, i18n: unknown): void;
@@ -13,7 +13,10 @@
13
13
  //
14
14
  // ⚠️ It touches `document` and `window` ON PURPOSE and that is why it is `platform/`: this is the layer whose job is the
15
15
  // browser. What it must never do is decide anything about language — it receives the locale and the tag already chosen.
16
- /** What a browser can offer `core/i18n`: the page effects of a change, and the language the person's browser is set to. */
16
+ /**
17
+ * What a browser can offer a translator: the page effects of a change, the language the person's browser is set to, and
18
+ * the page's signal that ANOTHER root changed the language (ADR-0232 D3: the language is the page's, and every root follows).
19
+ */
17
20
  export function localeHostHooks(doc, win, applyDom) {
18
21
  return {
19
22
  applied: (locale, tag) => {
@@ -26,9 +29,17 @@ export function localeHostHooks(doc, win, applyDom) {
26
29
  // ⚠️ `navigator` through the window, not the global: a host that hands the engine another window — a second document, an
27
30
  // iframe — means that window's language, and reading the global would silently answer for the wrong one.
28
31
  preferred: () => win.navigator?.language ?? null,
32
+ // 📌 On the window the root was GIVEN — its listener scope, so a disposed root stops following (ADR-0220).
33
+ follow: (react) => {
34
+ win.addEventListener('i18n:change', (e) => {
35
+ const locale = e.detail?.locale;
36
+ if (typeof locale === 'string')
37
+ react(locale);
38
+ });
39
+ },
29
40
  };
30
41
  }
31
- /** Hangs the i18n object on the window for tests and the preview, which is what `core/i18n` used to do to itself. */
42
+ /** Hangs a root's translator on the window for tests and the preview, which is what `core/i18n` used to do to itself. */
32
43
  export function exposeI18n(win, i18n) {
33
44
  win.__i18n = i18n;
34
45
  }
@@ -37,4 +37,28 @@ export interface ReadingInWorker {
37
37
  /** Lets the thread go. A transcription still running answers with the reason instead of never answering. */
38
38
  close(): void;
39
39
  }
40
+ /** The reading thread of the language being read in, kept between readings; see `keepOneThreadPerLanguage`. */
41
+ export interface ReadingThreads<T extends {
42
+ close(): void;
43
+ }> {
44
+ /**
45
+ * The thread for this language: the one kept when its base language is the same, else a new one from `open` — handed a
46
+ * `failed` to call when its opening fails. `open` is given at each call because the one form a bundler recognises for the
47
+ * thread's file has to sit where the host's `Worker` is (ADR-0232 D4), which is the caller's scope and not this module's.
48
+ */
49
+ forLanguage(language: string, open: (failed: () => void) => T): T;
50
+ /** Lets the kept thread go (the game left); the next `forLanguage` opens one again. */
51
+ close(): void;
52
+ }
53
+ /**
54
+ * ONE READING THREAD PER LANGUAGE, KEPT (issue #185; ADR-0225 erratum). Opening a thread compiles a model of up to 378 MiB, so a
55
+ * reading in the language already open reuses its thread, and a reading in another language (a child who switched: `pt-BR` and
56
+ * `pt-PT` are one model, `pt` and `en` are two) closes the old thread and opens the new one — one compiled model in memory at a
57
+ * time, which is what a school machine can hold.
58
+ * 🔴 A THREAD WHOSE OPENING FAILED IS FORGOTTEN: the next reading opens it again — the model may have come down since, and a
59
+ * thread kept dead would answer every later reading with the first one's failure.
60
+ */
61
+ export declare function keepOneThreadPerLanguage<T extends {
62
+ close(): void;
63
+ }>(): ReadingThreads<T>;
40
64
  export declare function createReadingInWorker(d: ReadingInWorkerDeps): ReadingInWorker;
@@ -8,6 +8,44 @@
8
8
  // 📌 THE WORKER IS OPENED AT THE FIRST READING AND KEPT, not opened per utterance: opening it means compiling the model again,
9
9
  // and the model is up to 378 MiB. It is closed when the engine lets the reading go, so a game that stops listening stops
10
10
  // paying for a thread.
11
+ /**
12
+ * ONE READING THREAD PER LANGUAGE, KEPT (issue #185; ADR-0225 erratum). Opening a thread compiles a model of up to 378 MiB, so a
13
+ * reading in the language already open reuses its thread, and a reading in another language (a child who switched: `pt-BR` and
14
+ * `pt-PT` are one model, `pt` and `en` are two) closes the old thread and opens the new one — one compiled model in memory at a
15
+ * time, which is what a school machine can hold.
16
+ * 🔴 A THREAD WHOSE OPENING FAILED IS FORGOTTEN: the next reading opens it again — the model may have come down since, and a
17
+ * thread kept dead would answer every later reading with the first one's failure.
18
+ */
19
+ export function keepOneThreadPerLanguage() {
20
+ let kept = null;
21
+ const baseOf = (tag) => tag.split('-')[0].toLowerCase();
22
+ return {
23
+ forLanguage(language, open) {
24
+ const base = baseOf(language);
25
+ if (kept?.base === base && kept.thread)
26
+ return kept.thread;
27
+ kept?.thread?.close();
28
+ const mine = { base, thread: null, dead: false };
29
+ kept = mine;
30
+ const thread = open(() => {
31
+ mine.dead = true;
32
+ if (kept === mine)
33
+ kept = null;
34
+ mine.thread?.close();
35
+ });
36
+ if (mine.dead) {
37
+ thread.close();
38
+ return thread;
39
+ } // it failed while opening: answered, and not kept
40
+ mine.thread = thread;
41
+ return thread;
42
+ },
43
+ close() {
44
+ kept?.thread?.close();
45
+ kept = null;
46
+ },
47
+ };
48
+ }
11
49
  export function createReadingInWorker(d) {
12
50
  const worker = d.spawn();
13
51
  let nextId = 1;
@@ -33,6 +33,27 @@ export declare function readingModelFor(language: string): ReadingModelPlan | nu
33
33
  * eight decades below the LOUDEST point of this window, not a constant; and the scale ends in (x + 4) / 4.
34
34
  */
35
35
  export declare function logMel(samples: Float32Array, melFilters: readonly (readonly number[])[]): Float32Array;
36
+ /** The part of a `config.json` that says how a streaming encoder cuts the wave. */
37
+ export interface WaveFrameConfig {
38
+ readonly encoder_config?: {
39
+ readonly frame_ms?: number;
40
+ readonly sample_rate?: number;
41
+ };
42
+ }
43
+ /**
44
+ * THE FRAME A STREAMING ENCODER CUTS THE WAVE INTO, in samples: `frame_ms` at `sample_rate`, from the model's own `config.json`
45
+ * — 5 ms at 16 kHz, 80, for both Moonshines. 1 where the config names no frame: a model without one takes any length.
46
+ */
47
+ export declare function waveFrameOf(config: WaveFrameConfig): number;
48
+ /**
49
+ * WHAT MOONSHINE STREAMING'S ENCODER EATS: the wave in WHOLE FRAMES. Its first node reshapes the samples to `[1, -1, 80]`, and
50
+ * onnxruntime refuses a length that is not a multiple of 80 — 📏 measured on both graphs (issue #185): a microphone hands over
51
+ * blocks of 4096, which make whole frames only five at a time, so most readings failed. The model's own processor pads with
52
+ * zeros on the right to a multiple of 80 (`preprocessor_config.json`: `pad_to_multiple_of`, `padding_side`, `padding_value`),
53
+ * and this is that step: under 5 ms of silence after the child finished, and her wave untouched. An empty recording becomes one
54
+ * frame of silence, because a graph handed nothing does not run; a wave already in whole frames is handed on as it came.
55
+ */
56
+ export declare function inWholeFrames(samples: Float32Array, frame: number): Float32Array;
36
57
  /** A `tokenizer.json`, in the parts a reading reads: the vocabulary, the added tokens, and which decoder undoes them. */
37
58
  export interface TokenizerFile {
38
59
  readonly model?: {
@@ -14,6 +14,7 @@
14
14
  // · THE MEL FILTERS ARE NOT COMPUTED HERE. Whisper's own `preprocessor_config.json` carries them (80 × 201), which is most of its
15
15
  // 185 KB, and the catalogue already fetches it: a filterbank rebuilt from a formula is a second opinion about the model's input.
16
16
  // · Moonshine takes the WAVEFORM. No spectrogram, no padding to 30 s — it is a different front end, not a variant of this one.
17
+ // ⚠️ But the streaming encoder takes it in WHOLE FRAMES of 5 ms (80 samples), and refuses any other length: `inWholeFrames`.
17
18
  /** 16 kHz mono, for every one of them: it is what all three were trained on, and resampling is the caller's job. */
18
19
  export const READING_RATE = 16_000;
19
20
  /** Whisper reads a 30 s window at a time, always: shorter is padded with silence, longer is cut. */
@@ -95,6 +96,30 @@ export function logMel(samples, melFilters) {
95
96
  out[i] = (Math.max(out[i], floor) + 4) / 4;
96
97
  return out;
97
98
  }
99
+ /**
100
+ * THE FRAME A STREAMING ENCODER CUTS THE WAVE INTO, in samples: `frame_ms` at `sample_rate`, from the model's own `config.json`
101
+ * — 5 ms at 16 kHz, 80, for both Moonshines. 1 where the config names no frame: a model without one takes any length.
102
+ */
103
+ export function waveFrameOf(config) {
104
+ const { frame_ms: ms, sample_rate: rate } = config.encoder_config ?? {};
105
+ return ms && rate ? Math.round((ms * rate) / 1000) : 1;
106
+ }
107
+ /**
108
+ * WHAT MOONSHINE STREAMING'S ENCODER EATS: the wave in WHOLE FRAMES. Its first node reshapes the samples to `[1, -1, 80]`, and
109
+ * onnxruntime refuses a length that is not a multiple of 80 — 📏 measured on both graphs (issue #185): a microphone hands over
110
+ * blocks of 4096, which make whole frames only five at a time, so most readings failed. The model's own processor pads with
111
+ * zeros on the right to a multiple of 80 (`preprocessor_config.json`: `pad_to_multiple_of`, `padding_side`, `padding_value`),
112
+ * and this is that step: under 5 ms of silence after the child finished, and her wave untouched. An empty recording becomes one
113
+ * frame of silence, because a graph handed nothing does not run; a wave already in whole frames is handed on as it came.
114
+ */
115
+ export function inWholeFrames(samples, frame) {
116
+ const whole = Math.max(1, Math.ceil(samples.length / frame)) * frame;
117
+ if (whole === samples.length)
118
+ return samples;
119
+ const out = new Float32Array(whole);
120
+ out.set(samples);
121
+ return out;
122
+ }
98
123
  /** A PERIODIC Hann window: a symmetric one moves every value. 400 numbers, computed once. */
99
124
  const HANN = Float64Array.from({ length: N_FFT }, (_, i) => 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / N_FFT));
100
125
  /**
@@ -9,14 +9,15 @@
9
9
  // THREE FAMILIES, and they differ in ways that matter:
10
10
  // · Whisper (pt) eats a log-mel spectrogram and opens with four tokens — «transcribe, in Portuguese, without timestamps» — and
11
11
  // refuses 88 tokens that are not speech. Without that opening it answers in whatever language it feels like.
12
- // · Moonshine (es) eats the waveform and opens with one token, in two decoder graphs like Whisper's.
12
+ // · Moonshine (es) eats the waveform, in whole frames of the length its config names, and opens with one token, in two decoder
13
+ // graphs like Whisper's.
13
14
  // · Moonshine (en) is the same model with ONE MERGED decoder graph, told which pass it is on by `use_cache_branch` and fed empty
14
15
  // caches on the first. It is the shape most easily written wrong from a guess, which is why it is traced too.
15
16
  //
16
17
  // ⚠️ THE CROSS-ATTENTION CACHE IS COMPUTED ONCE AND NEVER AGAIN. The first pass answers a `present.*.encoder.*` per layer that
17
18
  // depends only on the sound; feeding back the new one each step is how a loop gets slower with every token AND drifts.
18
19
  import { atDelivery, loadOnnxRuntime } from './onnx-runtime.js';
19
- import { logMel, nextToken, readingModelFor, readingTextOf, suppressedTokens, tokenIdOf, WHISPER_BANDS, WHISPER_FRAMES, } from './reading-model.js';
20
+ import { inWholeFrames, logMel, nextToken, readingModelFor, readingTextOf, suppressedTokens, tokenIdOf, waveFrameOf, WHISPER_BANDS, WHISPER_FRAMES, } from './reading-model.js';
20
21
  const MAX_TOKENS = 200;
21
22
  export async function loadReadingRuntime(d) {
22
23
  const plan = readingModelFor(d.language);
@@ -53,9 +54,10 @@ export async function loadReadingRuntime(d) {
53
54
  const eos = generation.eos_token_id ?? -1;
54
55
  const { heads, width } = cacheSizeOf(config);
55
56
  const ceiling = d.maxTokens ?? MAX_TOKENS;
57
+ const frame = waveFrameOf(config);
56
58
  return {
57
59
  async transcribe(samples) {
58
- const encoded = await encoder.run(soundFor(ort, plan, samples, melFilters));
60
+ const encoded = await encoder.run(soundFor(ort, plan, samples, melFilters, frame));
59
61
  const hidden = encoded[encoder.outputNames[0]];
60
62
  const shape = {
61
63
  ort, start, hidden, heads, width,
@@ -82,12 +84,15 @@ export async function loadReadingRuntime(d) {
82
84
  }
83
85
  /**
84
86
  * WHAT THE ENCODER HEARS. Whisper a log-mel picture of 30 s drawn with the model's own filters (80 bands × 3000 frames), Moonshine
85
- * the wave itself, every sample. The names are the graphs' own: a real session refuses any other.
87
+ * the wave itself, every sample, filled with silence to the whole frames its encoder cuts (`inWholeFrames`) — the microphone's
88
+ * length is not the model's. The names are the graphs' own: a real session refuses any other.
86
89
  */
87
- function soundFor(ort, plan, samples, melFilters) {
88
- return plan.input === 'log-mel'
89
- ? { input_features: new ort.Tensor('float32', logMel(samples, melFilters ?? []), [1, WHISPER_BANDS, WHISPER_FRAMES]) }
90
- : { input_values: new ort.Tensor('float32', samples, [1, samples.length]) };
90
+ function soundFor(ort, plan, samples, melFilters, frame) {
91
+ if (plan.input === 'log-mel') {
92
+ return { input_features: new ort.Tensor('float32', logMel(samples, melFilters ?? []), [1, WHISPER_BANDS, WHISPER_FRAMES]) };
93
+ }
94
+ const wave = inWholeFrames(samples, frame);
95
+ return { input_values: new ort.Tensor('float32', wave, [1, wave.length]) };
91
96
  }
92
97
  /** What one decoder step is handed: the opening on the first, then only the newest token, and the caches so far. */
93
98
  function stepInputs(shape, session, first, ids, past) {
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  import { recognitionRoute, type RecognitionApi, type OnDeviceAvailability, type RecognitionInstance } from './speech-recognition.js';
3
+ import type { VoiceChooser, ChooseSettings, ChoiceHeard } from './choose-by-voice.js';
3
4
  /** The browser's recognition object, with the parts a reading uses. Assigned handlers, as the API has them. */
4
5
  export interface ListeningSession extends RecognitionInstance {
5
6
  start(): void;
@@ -48,6 +49,11 @@ export interface ReadingDeps {
48
49
  readonly record?: (options: ListenOptions) => Promise<Float32Array>;
49
50
  readonly route?: typeof recognitionRoute;
50
51
  readonly createSession?: (api: RecognitionApi, language: string) => ListeningSession;
52
+ /**
53
+ * The closed-grammar recogniser a choice among shown options is heard by (`platform/choose-by-voice`, ADR-0256). Absent: a
54
+ * choice is REFUSED with its reason, like a reading with nothing to hear it.
55
+ */
56
+ readonly chooser?: VoiceChooser;
51
57
  }
52
58
  export interface ListenOptions {
53
59
  /** How long a silence ends the reading. Default 1.5 s: shorter cuts a child who is thinking between words. */
@@ -58,7 +64,12 @@ export interface ListenOptions {
58
64
  export interface Reading {
59
65
  /** Listens until the child goes quiet, and answers with what was read. */
60
66
  listen(options?: ListenOptions): Promise<Heard>;
61
- /** Gives the microphone back now; the reading in hand answers with what it has. */
67
+ /**
68
+ * THE CHILD SAYS ONE OF THE OPTIONS THE GAME SHOWS (ADR-0256): pass them as written, get back which one was said — heard by a
69
+ * recogniser that can only answer with one of them. For choosing; `listen()` is for reading aloud.
70
+ */
71
+ choose(options: readonly string[], settings?: ChooseSettings): Promise<ChoiceHeard>;
72
+ /** Gives the microphone back now; the reading or the choice in hand answers with what it has. */
62
73
  stop(): void;
63
74
  /** Can this device serve the child's language at all? Answers with the reason when it cannot. */
64
75
  ready(): Promise<{
@@ -92,7 +92,15 @@ export function createReading(d) {
92
92
  }
93
93
  return {
94
94
  ready,
95
- stop() { askToStop?.('asked'); },
95
+ stop() { askToStop?.('asked'); d.chooser?.stop(); },
96
+ async choose(options, settings) {
97
+ if (!d.chooser) {
98
+ once('sem-escolha', 'choosing by voice: nothing on this device hears a choice among options — the child answers with the '
99
+ + 'arrows only; the engine lends a chooser where it can open the command recogniser (ADR-0256)');
100
+ throw new Error('choosing by voice is not available here');
101
+ }
102
+ return d.chooser.choose(options, settings);
103
+ },
96
104
  async listen(options = {}) {
97
105
  if (inFlight)
98
106
  return inFlight; // one microphone, one reading: the second caller waits on the first
@@ -37,7 +37,11 @@ export type HeardCommand = {
37
37
  readonly kind: 'item';
38
38
  readonly name: string;
39
39
  };
40
- /** How a heard text is compared: lower case, letters and spaces only. */
40
+ /**
41
+ * A name or a heard text as a recogniser writes it: lower case, letters, digits and single spaces — and the ACCENTS KEPT, because
42
+ * a closed grammar word spelled without them is not in the model's vocabulary and is dropped (`input/voice-map`, «ação»).
43
+ * The digit stays so «Jogador 1» and «Jogador 2» never collapse into one name.
44
+ */
41
45
  export declare const spokenText: (t: string) => string;
42
46
  export interface CommandReader {
43
47
  /** The menu's item names now on screen (already as spoken). */
@@ -48,4 +52,9 @@ export interface CommandReader {
48
52
  */
49
53
  read(index: number, text: string, final: boolean): readonly HeardCommand[];
50
54
  }
55
+ /**
56
+ * What two spoken forms are COMPARED by: `spokenText` without its accents. The grammar asks the model for «ação» (it would drop
57
+ * «acao»), but a recogniser that writes «acao» back — or a browser recogniser with no closed vocabulary — still said «ação».
58
+ */
59
+ export declare const compareKey: (t: string) => string;
51
60
  export declare function createCommandReader(vocabulary: readonly string[]): CommandReader;
@@ -37,20 +37,39 @@ export function createOnDeviceRecognition(api, language) {
37
37
  rec.interimResults = true;
38
38
  return rec;
39
39
  }
40
- /** How a heard text is compared: lower case, letters and spaces only. */
41
- export const spokenText = (t) => t.toLowerCase().normalize('NFC').replace(/[^\p{L}\s]/gu, ' ').replace(/\s+/g, ' ').trim();
40
+ /**
41
+ * A name or a heard text as a recogniser writes it: lower case, letters, digits and single spaces — and the ACCENTS KEPT, because
42
+ * a closed grammar word spelled without them is not in the model's vocabulary and is dropped (`input/voice-map`, «ação»).
43
+ * The digit stays so «Jogador 1» and «Jogador 2» never collapse into one name.
44
+ */
45
+ export const spokenText = (t) => t.toLowerCase().normalize('NFC').replace(/[^\p{L}\p{N}\s]/gu, ' ').replace(/\s+/g, ' ').trim();
46
+ /**
47
+ * What two spoken forms are COMPARED by: `spokenText` without its accents. The grammar asks the model for «ação» (it would drop
48
+ * «acao»), but a recogniser that writes «acao» back — or a browser recogniser with no closed vocabulary — still said «ação».
49
+ */
50
+ export const compareKey = (t) => spokenText(t).normalize('NFD').replace(/\p{M}/gu, '');
51
+ /** The phrases a reader knows, by comparison key → the spoken form it answers with (the first one to claim the key). */
52
+ function byKey(phrases) {
53
+ const m = new Map();
54
+ for (const p of phrases) {
55
+ const k = compareKey(p);
56
+ if (k && !m.has(k))
57
+ m.set(k, spokenText(p));
58
+ }
59
+ return m;
60
+ }
42
61
  export function createCommandReader(vocabulary) {
43
- const spokenForms = vocabulary.map(spokenText);
44
- let spokenItems = [];
62
+ const words = byKey(vocabulary);
63
+ let items = new Map();
45
64
  const firedCount = new Map();
46
65
  const phrasesIn = (text) => {
47
- const p = spokenText(text).split(' ').filter(Boolean);
66
+ const p = compareKey(text).split(' ').filter(Boolean);
48
67
  const found = [];
49
68
  for (let i = 0; i < p.length;) {
50
69
  let matched = null;
51
70
  for (let n = p.length - i; n >= 1; n--) {
52
71
  const f = p.slice(i, i + n).join(' ');
53
- if (spokenItems.includes(f) || spokenForms.includes(f)) {
72
+ if (items.has(f) || words.has(f)) {
54
73
  matched = f;
55
74
  i += n;
56
75
  break;
@@ -64,16 +83,17 @@ export function createCommandReader(vocabulary) {
64
83
  return found;
65
84
  };
66
85
  return {
67
- items(names) { spokenItems = names.map(spokenText); },
86
+ items(names) { items = byKey(names); },
68
87
  read(index, text, final) {
69
88
  const found = phrasesIn(text);
70
89
  const alreadyFired = firedCount.get(index) ?? 0;
71
90
  const newCommands = [];
72
91
  for (let i = alreadyFired; i < found.length; i++) {
73
92
  const f = found[i];
74
- if (!final && i === found.length - 1 && spokenItems.some((o) => o !== f && o.startsWith(f + ' ')))
93
+ if (!final && i === found.length - 1 && [...items.keys()].some((o) => o !== f && o.startsWith(f + ' ')))
75
94
  break;
76
- newCommands.push(spokenItems.includes(f) ? { kind: 'item', name: f } : { kind: 'palavra', word: f });
95
+ const item = items.get(f);
96
+ newCommands.push(item !== undefined ? { kind: 'item', name: item } : { kind: 'palavra', word: words.get(f) });
77
97
  firedCount.set(index, i + 1);
78
98
  }
79
99
  if (final)
@@ -12,4 +12,13 @@ export interface GameVoice extends SpeechPort {
12
12
  soundOn(): boolean;
13
13
  volume(): number;
14
14
  }
15
- export declare function gameSay(voice: GameVoice, text: string): void;
15
+ /**
16
+ * Says `text`, a word the game gives in `language` (BCP-47), with a voice of that language chosen by the rule narration's parts
17
+ * use (ADR-0243 §2): the exact tag, else a voice of the same language. REQUIRED, because an optional language would default to
18
+ * one, and that default — pt-BR whatever the page said — is what ADR-0243 §4 removed.
19
+ *
20
+ * 🔴 A DEVICE THAT LISTS VOICES AND NONE OF THE LANGUAGE says nothing (§3): a wrong accent is learned. A device that lists NONE
21
+ * yet — the browser fills the list late — is asked by the tag alone, and it picks its own voice for the language.
22
+ * Volume ×1.4 so the speech sits above the effects, capped at 1.
23
+ */
24
+ export declare function gameSay(voice: GameVoice, text: string, language: string): void;
@@ -1,21 +1,28 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
- // platform/speech.ts — the LITERACY voice (gameSay): pt-BR speech that is ALWAYS on (independent of the mixer's
3
- // narration toggle), through the browser's native voice. It picks a pt-BR voice and avoids pt-PT. A helper that holds nothing:
4
- // it takes the browser's speech and the master sound as a parameter (ADR-0232 D4, erratum D3 point 2).
2
+ // platform/speech.ts — the LITERACY voice (gameSay): speech that is ALWAYS on (independent of the mixer's narration toggle),
3
+ // through the browser's native voice, in the language the game says its words are in (ADR-0243 §4). A helper that holds
4
+ // nothing: it takes the browser's speech and the master sound as a parameter (ADR-0232 D4, erratum D3 point 2).
5
5
  // The MENU's TTS (narrate/ttsSpeak and the Kokoro neural engine) is another path, gated by the mixer — `platform/tts`.
6
- function ptbrVoice(ss) {
6
+ import { voiceOfLanguage } from './voice-plan.js';
7
+ /** The device's voices, or none where the browser has not listed them yet or throws. */
8
+ function listedVoices(ss) {
7
9
  try {
8
- const vs = ss.getVoices() || [];
9
- if (!vs.length)
10
- return null;
11
- return vs.find((v) => /pt[-_]?br/i.test(v.lang)) || vs.find((v) => /pt/i.test(v.lang) && /bras|brazil/i.test(v.name)) || vs.find((v) => /pt/i.test(v.lang) && !/pt[-_]?pt/i.test(v.lang)) || null;
10
+ return ss.getVoices() || [];
12
11
  }
13
12
  catch (e) {
14
- return null;
13
+ return [];
15
14
  }
16
15
  }
17
- // FORCES pt-BR (not the mixer's voice, which may be pt-PT). Volume ×1.4 so the speech sits above the effects.
18
- export function gameSay(voice, text) {
16
+ /**
17
+ * Says `text`, a word the game gives in `language` (BCP-47), with a voice of that language chosen by the rule narration's parts
18
+ * use (ADR-0243 §2): the exact tag, else a voice of the same language. REQUIRED, because an optional language would default to
19
+ * one, and that default — pt-BR whatever the page said — is what ADR-0243 §4 removed.
20
+ *
21
+ * 🔴 A DEVICE THAT LISTS VOICES AND NONE OF THE LANGUAGE says nothing (§3): a wrong accent is learned. A device that lists NONE
22
+ * yet — the browser fills the list late — is asked by the tag alone, and it picks its own voice for the language.
23
+ * Volume ×1.4 so the speech sits above the effects, capped at 1.
24
+ */
25
+ export function gameSay(voice, text, language) {
19
26
  if (!text || !voice.soundOn())
20
27
  return;
21
28
  try {
@@ -23,9 +30,13 @@ export function gameSay(voice, text) {
23
30
  if (!ss)
24
31
  return;
25
32
  ss.cancel();
33
+ const listed = listedVoices(ss);
34
+ const own = voiceOfLanguage(language, listed.map((v) => ({ locale: v.lang.replace('_', '-'), engine: 'webspeech', voice: v.name })));
35
+ if (listed.length && !own)
36
+ return;
26
37
  const u = voice.utterance(text);
27
- u.lang = 'pt-BR';
28
- const v = ptbrVoice(ss);
38
+ u.lang = language;
39
+ const v = listed.find((x) => x.name === own?.voice);
29
40
  if (v)
30
41
  u.voice = v;
31
42
  u.volume = Math.min(1, voice.volume() * 1.4);