@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
@@ -2,12 +2,36 @@
2
2
  import type { Store } from './storage.js';
3
3
  import type { Translator } from '../core/i18n.js';
4
4
  import { type NeuralVoice } from './voice-plan.js';
5
- import { type LoadKokoro, type KokoroModule, type KokoroSession } from './kokoro.js';
5
+ import { type LoadKokoro } from './kokoro.js';
6
6
  import type { SpeechPort } from './speech.js';
7
- export type { LoadKokoro, KokoroModule, KokoroSession };
7
+ export type { LoadKokoro };
8
+ /**
9
+ * A PART OF A SPOKEN TEXT (ADR-0243 §1). Without `language` it is FRAME, read by the interface's voice; with a BCP-47 `language`
10
+ * it is CONTENT, read by a voice of that language and never by a voice of another one (§2) — an English word in a Portuguese
11
+ * activity is read by an English voice. A game already keeps the frame in keys and the content in `{params}`; this says which is which.
12
+ */
13
+ export interface SpokenPart {
14
+ readonly text: string;
15
+ readonly language?: string;
16
+ }
17
+ /** What narration speaks: a text in the interface's language, or parts spoken in order, each in its own language. */
18
+ export type SpokenText = string | readonly SpokenPart[];
19
+ /** How a narration meets the one being spoken. Absent: it cuts it, as every narration always has. */
20
+ export interface NarrateOptions {
21
+ /**
22
+ * THE PLAYER WHO ASKED, and then the narration takes turns with the other players' (ADR-0234, errata 2026-09-26 — the Dev: «em
23
+ * fila, não junto ao mesmo tempo com outro sonar em andamento», and «outro jogador não corta, mas um jogador corta a si
24
+ * mesmo»). Behind another player's narration still being spoken it waits, and is spoken when that one and each one waiting
25
+ * before it are over; a player's newer narration cuts that player's own being spoken, and replaces that player's own
26
+ * waiting, in its place in the line. The sonar's readings are spoken so. A narration that names no player still cuts
27
+ * whatever is being spoken, and ends the ones waiting too.
28
+ */
29
+ readonly seat?: number;
30
+ }
31
+ /** `finished` is called once the utterance is over, however it ends: what lets parts be spoken in order. */
8
32
  interface TtsEngine {
9
33
  id: string;
10
- speak: (text: string) => void;
34
+ speak: (text: string, finished?: () => void) => void;
11
35
  }
12
36
  export interface TtsCtx {
13
37
  /**
@@ -55,9 +79,18 @@ export interface TtsCtx {
55
79
  speech: SpeechPort;
56
80
  /** The clock a browser utterance and a neural load are timed with: the root's `performance.now`. REQUIRED (ADR-0232 D4). */
57
81
  now: () => number;
82
+ /**
83
+ * Where a line of `problems` goes (ADR-0169): the root's list. Written once per language a part asked for and the device has no
84
+ * voice for (ADR-0243 §3). Absent, nothing is reported — a `tts` built outside a root has no `problems` to write to.
85
+ */
86
+ report?: (line: string) => void;
58
87
  }
59
88
  export interface Tts {
60
- narrate: (text: string) => void;
89
+ /**
90
+ * The entry point, gated by the mixer: a text in the interface's language, or parts in order (ADR-0243 §1). It cuts what is
91
+ * being spoken, unless `how` names the player who asked — then it takes turns with the other players' (the sonar, ADR-0234).
92
+ */
93
+ narrate: (text: SpokenText, how?: NarrateOptions) => void;
61
94
  ttsSpeak: (text: string) => boolean;
62
95
  loadTTS: () => void;
63
96
  speakWebSpeech: (text: string) => boolean;
@@ -4,9 +4,12 @@
4
4
  // loader, the browser's speech, the clock and the `<audio>` maker are the root's, received here (ADR-0232 D4). narrate()
5
5
  // is the entry point, gated by the mixer's narration toggle (audioCat.tts.on) — independent of captions. The PANEL's side (the
6
6
  // engine and voice lists) lives in `ui/voice-settings` and reads and writes the choice through this module. Injection by closure.
7
+ //
8
+ // A SPOKEN TEXT CARRIES ITS LANGUAGE (ADR-0243): `narrate` takes a text, in the interface's language, or parts spoken in order —
9
+ // the frame in the interface's voice, the content in a voice of its own language (`voice-plan.voiceOfLanguage`).
7
10
  import { KEYS } from './storage-keys.js';
8
11
  import { createInterruptibleSpeech } from './interruptible-speech.js';
9
- import { voicesForLocale } from './voice-plan.js';
12
+ import { voicesForLocale, voiceOfLanguage, speaksLanguageOf, languageName } from './voice-plan.js';
10
13
  import { spokenWords, speechSeconds, speechPlaybackRate } from '../core/speech-rate.js';
11
14
  import { KOKORO_VOICES, tokenize, sentenceStyle, eFala, wavDe, } from './kokoro.js';
12
15
  // 📌 The only neural engine is Kokoro (ADR-0207): the engine's earlier neural engine left when the licence chain of its voices came
@@ -31,6 +34,10 @@ export function createTts(ctx) {
31
34
  };
32
35
  let kokoroBackend = null;
33
36
  let loadedVoice = null; // the voice the loaded engine speaks; another choice reloads it
37
+ /** The loaded Kokoro model's speaker, any of its voices; null until the model loads. */
38
+ let neural = null;
39
+ /** A narration's number: a newer one ends the parts an older one has still to speak (ADR-0243 §1). */
40
+ let narrationTurn = 0;
34
41
  // Each voice's words per minute, averaged over the utterances long enough to measure (`core/speech-rate`): what a short
35
42
  // utterance («Voltar») is played against. Neural voices by id; browser voices by name.
36
43
  const voiceAverage = new Map();
@@ -44,15 +51,16 @@ export function createTts(ctx) {
44
51
  * WEB SPEECH FIRST (ADR-0200; issue #190): the browser's voices for the language lead the list, so where the device already speaks
45
52
  * no neural voice is loaded; then Kokoro's where the game declared it speaks neurally (ADR-0216 §3) — the fallback.
46
53
  */
47
- const browserVoices = () => {
54
+ /** Every voice the browser lists, in every language: what a part in another language is matched against (ADR-0243 §2). */
55
+ const deviceVoices = () => {
48
56
  let list = [];
49
57
  try {
50
58
  list = ctx.speech.synth()?.getVoices() ?? [];
51
59
  }
52
60
  catch { /* no speech synthesis here */ }
53
- return voicesForLocale(bcp47(), list.map((v) => ({ locale: v.lang.replace('_', '-'), engine: 'webspeech', voice: 'webspeech:' + v.name })));
61
+ return list.map((v) => ({ locale: v.lang.replace('_', '-'), engine: 'webspeech', voice: 'webspeech:' + v.name }));
54
62
  };
55
- const availableVoices = () => [...browserVoices(), ...(ctx.neuralVoice ? voicesForLocale(bcp47(), KOKORO_VOICES) : [])];
63
+ const availableVoices = () => [...voicesForLocale(bcp47(), deviceVoices()), ...(ctx.neuralVoice ? voicesForLocale(bcp47(), KOKORO_VOICES) : [])];
56
64
  /** The browser voice an entry names, or null. */
57
65
  const browserVoice = (id) => {
58
66
  if (!id?.startsWith('webspeech:'))
@@ -64,10 +72,12 @@ export function createTts(ctx) {
64
72
  return null;
65
73
  }
66
74
  };
75
+ /**
76
+ * The interface's voice (ADR-0243 §2 over the language of now): the stored choice, else `voice-plan`'s `VOICE_ORDER` — the exact
77
+ * tag before the region, a browser voice before a neural one.
78
+ */
67
79
  function voiceInUse() {
68
- const list = availableVoices();
69
- const storedVoiceId = ctx.store.get(KEYS.ttsVoz, null);
70
- return list.find((v) => v.voice === storedVoiceId) ?? list[0] ?? null;
80
+ return voiceOfLanguage(bcp47(), availableVoices(), [ctx.store.get(KEYS.ttsVoz, null)]);
71
81
  }
72
82
  function chooseVoice(id) {
73
83
  if (!availableVoices().some((v) => v.voice === id))
@@ -77,36 +87,53 @@ export function createTts(ctx) {
77
87
  _ttsVoiceObj = browserVoice(id);
78
88
  return true;
79
89
  }
90
+ /**
91
+ * NARRATION FOLLOWS THE LANGUAGE OF NOW (ADR-0243 §5): the page's voice object when it speaks the language of now; one picked in
92
+ * another language is dropped at the first utterance after the switch, and the voice in use for the new language speaks instead.
93
+ */
94
+ function voiceObjectOfNow() {
95
+ if (_ttsVoiceObj && !speaksLanguageOf(_ttsVoiceObj.lang.replace('_', '-'), bcp47()))
96
+ _ttsVoiceObj = null;
97
+ return _ttsVoiceObj;
98
+ }
99
+ /**
100
+ * One browser utterance: `lang` is the tag the text is in and `voice` a voice of it, or null for the browser to choose by the tag.
101
+ * ADR-0183 §1: the browser's `rate` is a multiplier. The voice's words a minute at rate 1 is measured on its own utterances (start
102
+ * to end, so the silent ends count — an approximation the neural path does not need); until one is measured, 1.
103
+ */
104
+ function browserUtterance(text, lang, voice) {
105
+ const u = ctx.speech.utterance(text);
106
+ u.lang = lang;
107
+ if (voice)
108
+ u.voice = voice;
109
+ u.volume = Math.min(1, ctx.getVolume() * 1.4);
110
+ const voiceIdOf = 'webspeech:' + (voice?.name ?? lang);
111
+ const words = spokenWords(text);
112
+ const voiceBase = voiceAverage.get(voiceIdOf) ?? null;
113
+ u.rate = ctx.getSpeechPpm && voiceBase ? speechPlaybackRate(0, 0, ctx.getSpeechPpm(), voiceBase).rate : 1;
114
+ let startAt = 0;
115
+ u.onstart = () => { startAt = ctx.now(); };
116
+ u.onend = () => {
117
+ const seconds = startAt ? (ctx.now() - startAt) / 1000 : 0;
118
+ const measured = speechPlaybackRate(words, seconds, 150, null).voiceWpm;
119
+ if (measured)
120
+ media(voiceIdOf, measured / u.rate);
121
+ };
122
+ return u;
123
+ }
124
+ /**
125
+ * An utterance in the interface's language. `u.lang` follows the game's language: fixed at pt-BR, a game in English or Spanish
126
+ * would ask the browser for a PORTUGUESE voice for a text that is not Portuguese — and the result is not an accent, it is
127
+ * unintelligible: the wrong phonetics applied to the wrong letters.
128
+ */
129
+ const interfaceUtterance = (text) => browserUtterance(text, bcp47(), voiceObjectOfNow() ?? browserVoice(voiceInUse()?.voice));
80
130
  function speakWebSpeech(text) {
81
131
  try {
82
132
  const ss = ctx.speech.synth();
83
133
  if (!ss)
84
134
  return false;
85
135
  ss.cancel();
86
- // `u.lang` follows the game's language. Fixed at pt-BR, a game in English or Spanish would ask the browser for a
87
- // PORTUGUESE voice for a text that is not Portuguese — and the result is not an accent, it is unintelligible: the
88
- // wrong phonetics applied to the wrong letters.
89
- const voiceName = _ttsVoiceObj ?? browserVoice(voiceInUse()?.voice);
90
- const u = ctx.speech.utterance(text);
91
- u.lang = bcp47();
92
- if (voiceName)
93
- u.voice = voiceName;
94
- u.volume = Math.min(1, ctx.getVolume() * 1.4);
95
- // ADR-0183 §1: the browser's `rate` is a multiplier. The voice's words a minute at rate 1 is measured on its own utterances
96
- // (start to end, so the silent ends count — an approximation the neural path does not need); until one is measured, 1.
97
- const voiceIdOf = 'webspeech:' + (voiceName?.name ?? bcp47());
98
- const words = spokenWords(text);
99
- const voiceBase = voiceAverage.get(voiceIdOf) ?? null;
100
- u.rate = ctx.getSpeechPpm && voiceBase ? speechPlaybackRate(0, 0, ctx.getSpeechPpm(), voiceBase).rate : 1;
101
- let startAt = 0;
102
- u.onstart = () => { startAt = ctx.now(); };
103
- u.onend = () => {
104
- const seconds = startAt ? (ctx.now() - startAt) / 1000 : 0;
105
- const measured = speechPlaybackRate(words, seconds, 150, null).voiceWpm;
106
- if (measured)
107
- media(voiceIdOf, measured / u.rate);
108
- };
109
- ss.speak(u);
136
+ ss.speak(interfaceUtterance(text));
110
137
  return true;
111
138
  }
112
139
  catch (e) {
@@ -115,12 +142,14 @@ export function createTts(ctx) {
115
142
  }
116
143
  /**
117
144
  * A neural voice's utterances, whatever made the WAV (ADR-0183 §1): every utterance is measured — its words over its speech time,
118
- * silent ends trimmed — and played at the child's rate over the voice's, through a media element that keeps the pitch.
145
+ * silent ends trimmed — and played at the child's rate over the voice's, through a media element that keeps the pitch. Each
146
+ * request names its voice, so one loaded model reads the parts of every language it has a voice for (ADR-0243 §2).
119
147
  */
120
- function speakByWav(voiceId, produce) {
148
+ function speakByWav(produce) {
121
149
  return createInterruptibleSpeech({
122
- synthesize: async (text) => {
123
- const bytes = await produce(text);
150
+ synthesize: async (request) => {
151
+ const { text } = request, voiceId = request.voice.voice;
152
+ const bytes = await produce(request);
124
153
  const ac = ctx.ensureAC();
125
154
  if (!ac)
126
155
  throw new Error('AudioContext unavailable'); // the `catch` there handles it: this item is silent, the engine lives
@@ -173,14 +202,25 @@ export function createTts(ctx) {
173
202
  ctx.srSay(t('sr.tts.downloading'));
174
203
  load().then(async (mod) => {
175
204
  const [symbolIds, table] = await Promise.all([mod.vocabulary(), mod.voice(kv.voice)]);
176
- const synthesiseWith = async (kokoroSession, text) => {
177
- const ids = tokenize(await mod.phonemize(text, kv.espeak), symbolIds);
178
- return kokoroSession.synthesize(ids, sentenceStyle(table, ids.length - 2));
205
+ // Each voice's style table, fetched the first time an utterance asks for that voice; a failed fetch is tried again.
206
+ const tables = new Map([[kv.voice, Promise.resolve(table)]]);
207
+ const tableOf = (voice) => {
208
+ let styles = tables.get(voice.voice);
209
+ if (!styles) {
210
+ styles = mod.voice(voice.voice);
211
+ tables.set(voice.voice, styles);
212
+ styles.catch(() => tables.delete(voice.voice));
213
+ }
214
+ return styles;
215
+ };
216
+ const synthesiseWith = async (kokoroSession, text, voice) => {
217
+ const ids = tokenize(await mod.phonemize(text, voice.espeak), symbolIds);
218
+ return kokoroSession.synthesize(ids, sentenceStyle(await tableOf(voice), ids.length - 2));
179
219
  };
180
220
  let kokoroSession = null;
181
221
  try {
182
222
  const gpu = await mod.session('webgpu');
183
- if (eFala(await synthesiseWith(gpu, t('tts.kokoro.teste')))) {
223
+ if (eFala(await synthesiseWith(gpu, t('tts.kokoro.teste'), kv))) {
184
224
  kokoroSession = gpu;
185
225
  kokoroBackend = 'webgpu';
186
226
  }
@@ -191,8 +231,9 @@ export function createTts(ctx) {
191
231
  kokoroBackend = 'wasm';
192
232
  }
193
233
  const used = kokoroSession;
194
- const fala = speakByWav(kv.voice, async (text) => wavDe(await synthesiseWith(used, text)));
195
- ttsEngine = { id: 'kokoro', speak: (text) => { fala.speak(text); } };
234
+ const speaker = speakByWav(async (request) => wavDe(await synthesiseWith(used, request.text, request.voice)));
235
+ neural = speaker;
236
+ ttsEngine = { id: 'kokoro', speak: (text, finished) => { speaker.speak({ text, voice: kv }, finished); } };
196
237
  ttsLoading = false;
197
238
  try {
198
239
  ctx.speech.synth()?.cancel();
@@ -229,34 +270,215 @@ export function createTts(ctx) {
229
270
  }
230
271
  loadKokoroEngine(forVoice);
231
272
  }
273
+ /** The loaded neural engine when the interface speaks neurally; else it starts loading, and the browser speaks meanwhile. */
274
+ function interfaceEngine() {
275
+ if (engineSel() === 'webspeech')
276
+ return null;
277
+ // another voice picked, or the language changed, since the engine loaded: load the voice in use
278
+ const inUse = voiceInUse();
279
+ if (ttsEngine && inUse?.engine !== 'webspeech' && loadedVoice !== inUse?.voice) {
280
+ ttsEngine = null;
281
+ ttsFailed = false;
282
+ }
283
+ if (!ttsEngine)
284
+ loadTTS(); // the neural engine is downloading or unavailable → falls back
285
+ return ttsEngine;
286
+ }
232
287
  function ttsSpeak(text) {
233
- if (engineSel() !== 'webspeech') {
234
- // another voice picked, or the language changed, since the engine loaded: load the voice in use
235
- const inUse = voiceInUse();
236
- if (ttsEngine && inUse?.engine !== 'webspeech' && loadedVoice !== inUse?.voice) {
237
- ttsEngine = null;
238
- ttsFailed = false;
239
- }
240
- if (ttsEngine && ttsEngine.speak) {
241
- try {
242
- ttsEngine.speak(text);
243
- }
244
- catch (e) { /* noop */ }
245
- return true;
288
+ const engine = interfaceEngine();
289
+ if (engine) {
290
+ try {
291
+ engine.speak(text);
246
292
  }
247
- loadTTS(); // the neural engine is downloading or unavailable → falls back
293
+ catch (e) { /* noop */ }
294
+ return true;
248
295
  }
296
+ neural?.silence(); // a neural part still speaking gives way: the latest narration is the one that counts
249
297
  return speakWebSpeech(text); // immediate fallback: Web Speech, in the game's language
250
298
  }
251
- function narrate(text) {
299
+ /** A FRAME part (ADR-0243 §1): the interface's voice, as a text narrated alone; none while narration is locked (ADR-0185 §4). */
300
+ function frameSegment(text) {
301
+ if (availableVoices().length === 0)
302
+ return null;
303
+ const engine = interfaceEngine();
304
+ if (engine)
305
+ return { neural: (finished) => { engine.speak(text, finished); } };
306
+ return { browser: () => interfaceUtterance(text) };
307
+ }
308
+ /**
309
+ * A CONTENT part (ADR-0243 §2): a voice of its language in `voice-plan`'s `VOICE_ORDER` — the exact tag before the region, a
310
+ * browser voice before the neural ones this game asked for, which read only once the model is loaded; while it loads, the part starts the load and a browser voice of the language reads it if
311
+ * the device has one. `null`: the device has no voice for the language, and no voice of another language reads it instead.
312
+ */
313
+ function contentSegment(text, language) {
314
+ const chosen = chosenVoices();
315
+ const neuralVoices = ctx.neuralVoice && !ttsFailed ? KOKORO_VOICES : [];
316
+ const best = voiceOfLanguage(language, [...deviceVoices(), ...neuralVoices], chosen);
317
+ if (!best)
318
+ return noVoiceSegment(language);
319
+ const kokoro = KOKORO_VOICES.find((v) => v.voice === best.voice);
320
+ return kokoro ? neuralSegment(text, language, kokoro, chosen) : browserSegment(text, language, best);
321
+ }
322
+ /** The languages this root has already said it has no voice for, by primary language (ADR-0243 §3). */
323
+ const toldNoVoice = new Set();
324
+ /**
325
+ * NO VOICE FOR IT, NO WRONG VOICE (ADR-0243 §3): the part is not spoken — the text stays on the screen. Once per language, the
326
+ * interface's voice says so in its place, and `problems` gets one line naming the language and the fix.
327
+ */
328
+ function noVoiceSegment(language) {
329
+ const english = languageName(language, 'en');
330
+ const code = language.split('-')[0].toLowerCase();
331
+ if (toldNoVoice.has(code))
332
+ return null;
333
+ toldNoVoice.add(code);
334
+ ctx.report?.(`narration: the device lacks a voice for ${english} (${code}), so a part a game gave in ${english} is not read aloud `
335
+ + `and a child who listens does not hear it (the text stays on the screen) — install a voice for ${english} on the device, `
336
+ + `in its text-to-speech settings`);
337
+ return frameSegment(t('sr.tts.noVoiceForLanguage', { language: languageName(language, bcp47()) }));
338
+ }
339
+ /** The voices the child chose, in order of precedence: the page's voice object, then the stored choice (ADR-0243 §2). */
340
+ const chosenVoices = () => [_ttsVoiceObj ? 'webspeech:' + _ttsVoiceObj.name : null, ctx.store.get(KEYS.ttsVoz, null)];
341
+ const browserSegment = (text, language, voice) => ({ browser: () => browserUtterance(text, language, browserVoice(voice.voice)) });
342
+ /** A part a Kokoro voice reads; while the model loads, a browser voice of the language reads it if the device has one. */
343
+ function neuralSegment(text, language, voice, chosen) {
344
+ const speaker = neural;
345
+ if (speaker)
346
+ return { neural: (finished) => { speaker.speak({ text, voice }, finished); } };
347
+ if (!ttsLoading)
348
+ loadKokoroEngine(voice);
349
+ const meanwhile = voiceOfLanguage(language, deviceVoices(), chosen);
350
+ return meanwhile ? browserSegment(text, language, meanwhile) : null;
351
+ }
352
+ /**
353
+ * The utterances a narration waits on, held until their end is heard: an utterance nothing holds may be collected by the
354
+ * browser before it ends, and then its end is never heard — and a player's sonar would wait behind it forever (ADR-0234).
355
+ */
356
+ const heldUntilOver = new Set();
357
+ /** Resolves when an utterance is over, however it ends — set before it is queued. */
358
+ const utteranceOver = (u) => new Promise((over) => {
359
+ heldUntilOver.add(u);
360
+ const done = () => { heldUntilOver.delete(u); over(); };
361
+ const measure = u.onend;
362
+ u.onend = function (e) { measure?.call(this, e); done(); };
363
+ u.onerror = done;
364
+ });
365
+ /**
366
+ * Queues the browser parts from `from` on at once; `over` resolves when the last is over, when a part follows them or the
367
+ * caller waits for the end (`untilOver`). A device that refuses to speak has nothing to wait for: the rest goes on.
368
+ */
369
+ function queueBrowserParts(ss, segments, from, untilOver) {
370
+ const queued = [];
371
+ let i = from;
372
+ for (let s = segments[i]; s && 'browser' in s; s = segments[++i])
373
+ queued.push(s.browser());
374
+ const last = queued.at(-1);
375
+ const over = ss && last && (i < segments.length || untilOver) ? utteranceOver(last) : null;
376
+ try {
377
+ for (const u of queued)
378
+ ss?.speak(u);
379
+ }
380
+ catch (e) {
381
+ return { next: i, over: null };
382
+ }
383
+ return { next: i, over };
384
+ }
385
+ /**
386
+ * THE PARTS, IN ORDER (ADR-0243 §1). Browser utterances go into the browser's own queue at once, each with its tag and voice; a
387
+ * neural part waits for what is before it to end, and what follows waits for it. A newer narration ends the rest. `untilOver`:
388
+ * resolves only when the last part is over, for a narration waiting its turn behind this one.
389
+ */
390
+ async function speakInOrder(segments, turn, untilOver = false) {
391
+ const ss = ctx.speech.synth();
392
+ try {
393
+ ss?.cancel();
394
+ }
395
+ catch (e) { /* noop */ }
396
+ neural?.silence();
397
+ let i = 0;
398
+ while (i < segments.length && turn === narrationTurn) {
399
+ const first = segments[i];
400
+ if ('neural' in first) {
401
+ await new Promise((over) => { first.neural(over); });
402
+ i++;
403
+ continue;
404
+ }
405
+ const queuedParts = queueBrowserParts(ss, segments, i, untilOver);
406
+ i = queuedParts.next;
407
+ if (queuedParts.over)
408
+ await queuedParts.over;
409
+ }
410
+ }
411
+ /** The players' narrations waiting behind the one being spoken (ADR-0234), at most one per player, in the order they asked. */
412
+ const waiting = [];
413
+ /** The `narrationTurn` of the player's narration being spoken, and whose it is; any narration since has taken the turn. */
414
+ let turnSpeaking = -1, seatSpeaking = -1;
415
+ /**
416
+ * A PLAYER'S NARRATION TAKES TURNS (ADR-0234, errata 2026-09-26): behind ANOTHER player's still being spoken it waits — replacing
417
+ * that player's own waiting one, in its place — and is spoken when the line reaches it. With the SAME player's being spoken, or
418
+ * none, it starts at once and cuts what is spoken now, as every narration does; the others waiting keep waiting behind it. A
419
+ * narration that names no player takes the turn, so the next player's finds none being spoken and nobody waiting.
420
+ */
421
+ function narrateInTurn(seat, segments) {
422
+ _narrateCount++;
423
+ const speakingNow = turnSpeaking === narrationTurn;
424
+ if (speakingNow && seat !== seatSpeaking) {
425
+ const own = waiting.find((w) => w.seat === seat);
426
+ if (own)
427
+ own.segments = segments;
428
+ else
429
+ waiting.push({ seat, segments });
430
+ return;
431
+ }
432
+ if (!speakingNow)
433
+ waiting.length = 0; // their turn was ended by a narration that named no player
434
+ void speakInTurn({ seat, segments }, ++narrationTurn);
435
+ }
436
+ async function speakInTurn(first, turn) {
437
+ turnSpeaking = turn;
438
+ for (let next = first; next; next = waiting.shift()) {
439
+ seatSpeaking = next.seat;
440
+ await speakInOrder(next.segments, turn, true);
441
+ if (turn !== narrationTurn)
442
+ return; // cut: the line is the newer narration's now
443
+ }
444
+ turnSpeaking = -1;
445
+ seatSpeaking = -1;
446
+ }
447
+ function narrate(text, how = {}) {
252
448
  const cat = ctx.getAudioCat();
253
- if (!ctx.getSoundOn() || !cat || !cat.tts || !cat.tts.on || !text)
449
+ if (!ctx.getSoundOn() || !cat || !cat.tts || !cat.tts.on)
450
+ return;
451
+ if (typeof text !== 'string') {
452
+ narrateParts(text, how);
453
+ return;
454
+ }
455
+ if (!text)
254
456
  return;
255
457
  if (availableVoices().length === 0)
256
458
  return; // no voice speaks this language: narration is locked (ADR-0185 §4)
459
+ if (how.seat !== undefined) {
460
+ const frame = frameSegment(text);
461
+ if (frame)
462
+ narrateInTurn(how.seat, [frame]);
463
+ return;
464
+ }
465
+ narrationTurn++;
257
466
  _narrateCount++;
258
467
  ttsSpeak(text);
259
468
  }
469
+ function narrateParts(parts, how) {
470
+ const segments = parts.filter((p) => p.text)
471
+ .map((p) => (p.language ? contentSegment(p.text, p.language) : frameSegment(p.text)))
472
+ .filter((s) => s !== null);
473
+ if (!segments.length)
474
+ return;
475
+ if (how.seat !== undefined) {
476
+ narrateInTurn(how.seat, segments);
477
+ return;
478
+ }
479
+ _narrateCount++;
480
+ void speakInOrder(segments, ++narrationTurn);
481
+ }
260
482
  return {
261
483
  narrate, ttsSpeak, loadTTS, speakWebSpeech,
262
484
  getEngineSel: engineSel, setEngineSel: (v) => { ttsEngineSelExplicito = v; },
@@ -18,8 +18,12 @@ export interface VoiceListenerDeps {
18
18
  readonly createContext: () => AudioContextLike;
19
19
  /** What the recogniser has heard so far in this utterance. */
20
20
  readonly onPartial: (text: string) => void;
21
- /** The utterance ended: whoever counts what was already answered for starts a new sentence. */
22
- readonly onFinal?: () => void;
21
+ /**
22
+ * The utterance ended, with the recogniser's last word on it: a name the partials held back because another item's name
23
+ * continued it («voltar» while «voltar ao jogo» could follow) is decided HERE (ADR-0194 §3), and whoever counts what was
24
+ * already answered for starts a new sentence. `''` when the recogniser heard nothing it knows.
25
+ */
26
+ readonly onFinal?: (text: string) => void;
23
27
  }
24
28
  export interface VoiceListener {
25
29
  /** The words the recogniser may answer with, from now on. A new grammar is a new recogniser; the microphone stays open. */
@@ -31,7 +31,7 @@ export async function startVoiceListening(d) {
31
31
  const listen = (r) => {
32
32
  r.on('partialresult', (heard) => { const t = heard.result.partial; if (t)
33
33
  d.onPartial(t); });
34
- r.on('result', () => { d.onFinal?.(); });
34
+ r.on('result', (heard) => { d.onFinal?.(heard.result.text ?? ''); });
35
35
  };
36
36
  listen(recognizer);
37
37
  const stream = await getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, channelCount: 1 } });
@@ -11,3 +11,16 @@ export interface NeuralVoice {
11
11
  * is not offered: Portuguese text through English phonemes is noise.
12
12
  */
13
13
  export declare function voicesForLocale(tag: string, catalogue: readonly NeuralVoice[]): readonly NeuralVoice[];
14
+ /**
15
+ * A language's name, written in another language: `languageName('en-GB', 'pt-BR')` is «inglês» — the primary language, because a
16
+ * voice of the same language would have read the part (ADR-0243 §3). Where the platform cannot name it, the tag's language code.
17
+ */
18
+ export declare function languageName(tag: string, inLanguage: string): string;
19
+ /** Does a voice tagged `locale` speak the language of `tag`? By the primary language, as `voicesForLocale` matches. */
20
+ export declare function speaksLanguageOf(locale: string, tag: string): boolean;
21
+ /**
22
+ * THE VOICE OF A LANGUAGE (ADR-0243 §2): the child's chosen voice when it speaks that language; else the first voice `VOICE_ORDER`
23
+ * reaches — the exact tag before the region, a browser voice before a neural one. `chosen` comes in order of precedence.
24
+ * 🔴 No voice of another language is ever returned: `null` means the list has none for it, and the part is not spoken (§3).
25
+ */
26
+ export declare function voiceOfLanguage(tag: string, voices: readonly NeuralVoice[], chosen?: readonly (string | null | undefined)[]): NeuralVoice | null;
@@ -9,6 +9,61 @@
9
9
  * is not offered: Portuguese text through English phonemes is noise.
10
10
  */
11
11
  export function voicesForLocale(tag, catalogue) {
12
- const language = (tag.split('-')[0] ?? '').toLowerCase();
13
- return catalogue.filter((v) => (v.locale.split('-')[0] ?? '').toLowerCase() === language);
12
+ return catalogue.filter((v) => speaksLanguageOf(v.locale, tag));
13
+ }
14
+ /** The primary language of a BCP-47 tag, lower case: `pt` of `pt-BR`. A browser's `en_US` is normalised by whoever reads it. */
15
+ const primaryLanguage = (tag) => (tag.split('-')[0] ?? '').toLowerCase();
16
+ /**
17
+ * A language's name, written in another language: `languageName('en-GB', 'pt-BR')` is «inglês» — the primary language, because a
18
+ * voice of the same language would have read the part (ADR-0243 §3). Where the platform cannot name it, the tag's language code.
19
+ */
20
+ export function languageName(tag, inLanguage) {
21
+ const code = primaryLanguage(tag);
22
+ try {
23
+ return new Intl.DisplayNames([inLanguage], { type: 'language' }).of(code) ?? code;
24
+ }
25
+ catch {
26
+ return code;
27
+ }
28
+ }
29
+ /** Does a voice tagged `locale` speak the language of `tag`? By the primary language, as `voicesForLocale` matches. */
30
+ export function speaksLanguageOf(locale, tag) {
31
+ return primaryLanguage(locale) === primaryLanguage(tag);
32
+ }
33
+ /** The engine of a browser (Web Speech) voice in a list; every other engine is neural (ADR-0200). */
34
+ const BROWSER_ENGINE = 'webspeech';
35
+ const browser = (v) => v.engine === BROWSER_ENGINE;
36
+ const exactTag = (v, tag) => v.locale.toLowerCase() === tag.toLowerCase();
37
+ /**
38
+ * THE VOICE ORDER (ADR-0243 §2, the two errata of 2026-09-26), after the child's chosen voice. A BROWSER voice before a NEURAL one
39
+ * (the Dev: «Se a do navegador (mais leve) pode ser usada, a do navegador, senão ganha a voz neural (backup)»), but the exact tag
40
+ * before the region: asked whether a pt-PT browser voice reads a pt-BR part or the neural pt-BR voice does, the Dev: «Rede neural
41
+ * pt-BR.» So a voice of the same language and another region reads only where no voice of the exact tag exists, browser first.
42
+ * Every step is tried over the whole list, in order; within a step the list's own order decides. The interface's own voice
43
+ * follows the same order (`platform/tts`'s `voiceInUse`).
44
+ */
45
+ const VOICE_ORDER = [
46
+ (v, tag) => browser(v) && exactTag(v, tag),
47
+ (v, tag) => !browser(v) && exactTag(v, tag),
48
+ (v) => browser(v),
49
+ (v) => !browser(v),
50
+ ];
51
+ /**
52
+ * THE VOICE OF A LANGUAGE (ADR-0243 §2): the child's chosen voice when it speaks that language; else the first voice `VOICE_ORDER`
53
+ * reaches — the exact tag before the region, a browser voice before a neural one. `chosen` comes in order of precedence.
54
+ * 🔴 No voice of another language is ever returned: `null` means the list has none for it, and the part is not spoken (§3).
55
+ */
56
+ export function voiceOfLanguage(tag, voices, chosen = []) {
57
+ const own = voicesForLocale(tag, voices);
58
+ for (const id of chosen) {
59
+ const picked = id ? own.find((v) => v.voice === id) : undefined;
60
+ if (picked)
61
+ return picked;
62
+ }
63
+ for (const step of VOICE_ORDER) {
64
+ const found = own.find((v) => step(v, tag));
65
+ if (found)
66
+ return found;
67
+ }
68
+ return null;
14
69
  }
@@ -42,10 +42,24 @@ export interface VoskDeps {
42
42
  * put in the page once per address. REQUIRED, and injected so a gate never imports 3 MiB of wasm loader.
43
43
  */
44
44
  readonly loadBundle: (absoluteUrl: string) => Promise<VoskApi>;
45
+ /**
46
+ * The host's `fetch`, lent by the root, to read the MODEL'S VOCABULARY from the archive the recogniser opens
47
+ * (`platform/vosk-vocabulary`, ADR-0194 §4): the runtime keeps it inside its worker and tells the page nothing. Optional; without
48
+ * it the load carries no `vocabulary`, and a name with a word the model lacks goes unreported.
49
+ */
50
+ readonly fetch?: (absoluteUrl: string) => Promise<{
51
+ readonly ok: boolean;
52
+ readonly body: ReadableStream<BufferSource> | null;
53
+ }>;
45
54
  }
46
55
  export type VoskLoad = {
47
56
  readonly ok: true;
48
57
  readonly model: VoskModel;
58
+ /**
59
+ * The words the loaded model knows, read from its archive once asked — `null` when they cannot be read. Present only when the
60
+ * deps lent a `fetch`.
61
+ */
62
+ readonly vocabulary?: () => Promise<ReadonlySet<string> | null>;
49
63
  }
50
64
  /** The catalogue ids that are not in the checked cache — `language` when the project has no model for it. */
51
65
  | {
@@ -62,6 +76,17 @@ export type VoskLoad = {
62
76
  * memo at module level was one per PAGE, shared by roots that share nothing else. `load` is the root's `import(url)`.
63
77
  */
64
78
  export declare function createBundleLoader(load: (absoluteUrl: string) => Promise<unknown>): (absoluteUrl: string) => Promise<VoskApi>;
79
+ /**
80
+ * ONE MODEL PER LANGUAGE, LOADED ONCE AND KEPT (ADR-0256): the 👄 and a choice by voice ask the same `load`, and a listener that
81
+ * stops no longer ends the model — the one handed out has a `terminate` that does nothing; the real one is ended when it is
82
+ * pushed out (more than `keep` languages, the oldest first) or by `closeAll()`, the root's end.
83
+ * 📏 Why: every start used to spawn a worker and unpack ~40 MiB of model, and every choice started two — the Dev's machine froze
84
+ * on 2026-09-27. A load that failed is forgotten, so a model that comes down later is found.
85
+ */
86
+ export declare function keepVoskModels(load: (d: VoskDeps) => Promise<VoskLoad>, keep?: number): {
87
+ load(d: VoskDeps): Promise<VoskLoad>;
88
+ closeAll(): void;
89
+ };
65
90
  /**
66
91
  * Opens the recogniser for the child's language, or says what is missing.
67
92
  *