@the-inclusionist/engine 9.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.
- package/README.md +120 -78
- package/app/css/style.css +761 -377
- package/app/public/vendor/fonts/playwrite-cu.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-es-deco.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-es.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-gb-j.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-gb-s.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-pe.woff2 +0 -0
- package/app/public/vendor/fonts/playwrite-pt.woff2 +0 -0
- package/app/public/vendor/fonts-licences/NOTICE.txt +166 -0
- package/app/public/vendor/fonts-licences/OFL-1.1.txt +86 -0
- package/app/public/vendor/fonts.css +36 -84
- package/dist-pkg/boot/create-game.d.ts +499 -219
- package/dist-pkg/boot/create-game.js +3944 -562
- package/dist-pkg/core/a11y-sr.d.ts +25 -3
- package/dist-pkg/core/a11y-sr.js +38 -19
- package/dist-pkg/core/accessible-label.d.ts +17 -0
- package/dist-pkg/core/accessible-label.js +37 -0
- package/dist-pkg/core/accommodation-subjects.d.ts +26 -0
- package/dist-pkg/core/accommodation-subjects.js +32 -0
- package/dist-pkg/core/accommodations.d.ts +125 -0
- package/dist-pkg/core/accommodations.js +195 -0
- package/dist-pkg/core/actions.d.ts +108 -55
- package/dist-pkg/core/actions.js +122 -71
- package/dist-pkg/core/calm-mode.d.ts +31 -0
- package/dist-pkg/core/calm-mode.js +47 -0
- package/dist-pkg/core/camera-cycle.d.ts +15 -0
- package/dist-pkg/core/camera-cycle.js +7 -0
- package/dist-pkg/core/caption-duration.d.ts +15 -0
- package/dist-pkg/core/caption-duration.js +19 -0
- package/dist-pkg/core/cartridge-problems.d.ts +34 -0
- package/dist-pkg/core/cartridge-problems.js +53 -0
- package/dist-pkg/core/constants.d.ts +0 -54
- package/dist-pkg/core/constants.js +9 -76
- package/dist-pkg/core/contract.d.ts +142 -164
- package/dist-pkg/core/contract.js +222 -208
- package/dist-pkg/core/dom-query.d.ts +4 -4
- package/dist-pkg/core/dom-query.js +12 -20
- package/dist-pkg/core/entity.d.ts +62 -77
- package/dist-pkg/core/entity.js +18 -32
- package/dist-pkg/core/escape-html.d.ts +9 -10
- package/dist-pkg/core/escape-html.js +16 -20
- package/dist-pkg/core/flash-threshold.d.ts +17 -0
- package/dist-pkg/core/flash-threshold.js +113 -0
- package/dist-pkg/core/game-speed.d.ts +11 -0
- package/dist-pkg/core/game-speed.js +16 -0
- package/dist-pkg/core/genres.d.ts +20 -0
- package/dist-pkg/core/genres.js +44 -0
- package/dist-pkg/core/i18n.d.ts +71 -60
- package/dist-pkg/core/i18n.js +186 -200
- package/dist-pkg/core/loop.d.ts +17 -6
- package/dist-pkg/core/loop.js +26 -26
- package/dist-pkg/core/pause-icon-catalogue.d.ts +15 -0
- package/dist-pkg/core/pause-icon-catalogue.js +54 -0
- package/dist-pkg/core/ring.d.ts +17 -0
- package/dist-pkg/core/ring.js +29 -0
- package/dist-pkg/core/rng.d.ts +7 -12
- package/dist-pkg/core/rng.js +23 -53
- package/dist-pkg/core/route.d.ts +26 -26
- package/dist-pkg/core/route.js +126 -121
- package/dist-pkg/core/scenes.d.ts +37 -40
- package/dist-pkg/core/scenes.js +41 -43
- package/dist-pkg/core/screens.d.ts +4 -4
- package/dist-pkg/core/screens.js +11 -16
- package/dist-pkg/core/session-clock.d.ts +23 -0
- package/dist-pkg/core/session-clock.js +37 -0
- package/dist-pkg/core/setting-defaults.d.ts +61 -0
- package/dist-pkg/core/setting-defaults.js +66 -0
- package/dist-pkg/core/speech-rate.d.ts +29 -0
- package/dist-pkg/core/speech-rate.js +47 -0
- package/dist-pkg/core/state.d.ts +118 -118
- package/dist-pkg/core/state.js +322 -297
- package/dist-pkg/core/visual-cycles.d.ts +30 -0
- package/dist-pkg/core/visual-cycles.js +52 -0
- package/dist-pkg/core/visual-state.d.ts +13 -0
- package/dist-pkg/core/visual-state.js +6 -0
- package/dist-pkg/educational/activities-registry.d.ts +1 -1
- package/dist-pkg/educational/activities-registry.js +9 -8
- package/dist-pkg/educational/adaptive-engine.d.ts +11 -11
- package/dist-pkg/educational/adaptive-engine.js +17 -17
- package/dist-pkg/educational/segment-bar.d.ts +12 -12
- package/dist-pkg/educational/segment-bar.js +9 -9
- package/dist-pkg/i18n/en.js +324 -223
- package/dist-pkg/i18n/es.js +323 -223
- package/dist-pkg/i18n/pt.js +348 -226
- package/dist-pkg/input/default-bindings.d.ts +37 -38
- package/dist-pkg/input/default-bindings.js +127 -144
- package/dist-pkg/input/devices.d.ts +7 -7
- package/dist-pkg/input/devices.js +17 -16
- package/dist-pkg/input/edges.d.ts +23 -32
- package/dist-pkg/input/edges.js +23 -28
- package/dist-pkg/input/empathy-filter.d.ts +22 -0
- package/dist-pkg/input/empathy-filter.js +34 -0
- package/dist-pkg/input/face-map.d.ts +96 -0
- package/dist-pkg/input/face-map.js +255 -0
- package/dist-pkg/input/face-signals.d.ts +40 -0
- package/dist-pkg/input/face-signals.js +48 -0
- package/dist-pkg/input/gamepad.d.ts +195 -145
- package/dist-pkg/input/gamepad.js +383 -556
- package/dist-pkg/input/gaze-cycle.d.ts +64 -0
- package/dist-pkg/input/gaze-cycle.js +133 -0
- package/dist-pkg/input/gaze-relative.d.ts +68 -0
- package/dist-pkg/input/gaze-relative.js +142 -0
- package/dist-pkg/input/hand-map.d.ts +28 -0
- package/dist-pkg/input/hand-map.js +142 -0
- package/dist-pkg/input/input-cooldown.d.ts +17 -0
- package/dist-pkg/input/input-cooldown.js +39 -0
- package/dist-pkg/input/key-default.d.ts +30 -0
- package/dist-pkg/input/key-default.js +60 -0
- package/dist-pkg/input/keyboard-runtime.d.ts +15 -21
- package/dist-pkg/input/keyboard-runtime.js +23 -27
- package/dist-pkg/input/keyboard.d.ts +68 -63
- package/dist-pkg/input/keyboard.js +83 -105
- package/dist-pkg/input/keydown.d.ts +123 -137
- package/dist-pkg/input/keydown.js +284 -289
- package/dist-pkg/input/latch-edge.d.ts +33 -21
- package/dist-pkg/input/latch-edge.js +34 -35
- package/dist-pkg/input/latch-scope.d.ts +51 -50
- package/dist-pkg/input/latch-scope.js +61 -82
- package/dist-pkg/input/latch-store.d.ts +26 -30
- package/dist-pkg/input/latch-store.js +42 -50
- package/dist-pkg/input/latch-sync.d.ts +29 -29
- package/dist-pkg/input/latch-sync.js +25 -25
- package/dist-pkg/input/latch.d.ts +14 -17
- package/dist-pkg/input/latch.js +29 -37
- package/dist-pkg/input/pad-defaults.d.ts +13 -16
- package/dist-pkg/input/pad-defaults.js +21 -28
- package/dist-pkg/input/pad-reading.d.ts +76 -0
- package/dist-pkg/input/pad-reading.js +169 -0
- package/dist-pkg/input/pad-wizard.d.ts +99 -0
- package/dist-pkg/input/pad-wizard.js +256 -0
- package/dist-pkg/input/pointer-space.d.ts +19 -20
- package/dist-pkg/input/pointer-space.js +27 -32
- package/dist-pkg/input/pointer.d.ts +39 -40
- package/dist-pkg/input/pointer.js +42 -45
- package/dist-pkg/input/state.d.ts +101 -89
- package/dist-pkg/input/state.js +54 -134
- package/dist-pkg/input/switch-scan.d.ts +59 -0
- package/dist-pkg/input/switch-scan.js +67 -0
- package/dist-pkg/input/synthetic-source.d.ts +42 -0
- package/dist-pkg/input/synthetic-source.js +57 -0
- package/dist-pkg/input/touch-bindings.d.ts +127 -116
- package/dist-pkg/input/touch-bindings.js +158 -173
- package/dist-pkg/input/touch.d.ts +147 -72
- package/dist-pkg/input/touch.js +362 -126
- package/dist-pkg/input/transport-in-use.d.ts +107 -0
- package/dist-pkg/input/transport-in-use.js +130 -0
- package/dist-pkg/input/transports.d.ts +90 -117
- package/dist-pkg/input/transports.js +104 -106
- package/dist-pkg/input/virtual-controller.d.ts +82 -0
- package/dist-pkg/input/virtual-controller.js +69 -0
- package/dist-pkg/input/vocabulary-migration.d.ts +50 -49
- package/dist-pkg/input/vocabulary-migration.js +80 -79
- package/dist-pkg/input/voice-map.d.ts +59 -0
- package/dist-pkg/input/voice-map.js +123 -0
- package/dist-pkg/platform/audio-ambient.d.ts +13 -2
- package/dist-pkg/platform/audio-ambient.js +14 -14
- package/dist-pkg/platform/audio-earcons.d.ts +20 -19
- package/dist-pkg/platform/audio-earcons.js +51 -42
- package/dist-pkg/platform/audio-jingles.js +9 -10
- package/dist-pkg/platform/audio-mixer.d.ts +14 -2
- package/dist-pkg/platform/audio-mixer.js +31 -39
- package/dist-pkg/platform/audio-sonar.d.ts +49 -152
- package/dist-pkg/platform/audio-sonar.js +124 -302
- package/dist-pkg/platform/audio.d.ts +62 -19
- package/dist-pkg/platform/audio.js +193 -168
- package/dist-pkg/platform/choose-by-voice.d.ts +70 -0
- package/dist-pkg/platform/choose-by-voice.js +157 -0
- package/dist-pkg/platform/flash-sampler.d.ts +19 -0
- package/dist-pkg/platform/flash-sampler.js +79 -0
- package/dist-pkg/platform/font-library.d.ts +77 -0
- package/dist-pkg/platform/font-library.js +98 -0
- package/dist-pkg/platform/font-library.json +721 -0
- package/dist-pkg/platform/heavy-catalogue.d.ts +77 -0
- package/dist-pkg/platform/heavy-catalogue.js +256 -0
- package/dist-pkg/platform/heavy-mirror.d.ts +20 -0
- package/dist-pkg/platform/heavy-mirror.js +73 -0
- package/dist-pkg/platform/heavy.d.ts +136 -0
- package/dist-pkg/platform/heavy.js +324 -0
- package/dist-pkg/platform/interruptible-speech.d.ts +23 -16
- package/dist-pkg/platform/interruptible-speech.js +77 -54
- package/dist-pkg/platform/kokoro-port.d.ts +27 -0
- package/dist-pkg/platform/kokoro-port.js +50 -0
- package/dist-pkg/platform/kokoro-runtime.d.ts +20 -0
- package/dist-pkg/platform/kokoro-runtime.js +52 -0
- package/dist-pkg/platform/kokoro.d.ts +70 -0
- package/dist-pkg/platform/kokoro.js +133 -0
- package/dist-pkg/platform/listener-scope.d.ts +12 -0
- package/dist-pkg/platform/listener-scope.js +90 -0
- package/dist-pkg/platform/locale-host.d.ts +14 -0
- package/dist-pkg/platform/locale-host.js +45 -0
- package/dist-pkg/platform/microphone.d.ts +51 -0
- package/dist-pkg/platform/microphone.js +118 -0
- package/dist-pkg/platform/onnx-runtime.d.ts +34 -0
- package/dist-pkg/platform/onnx-runtime.js +42 -0
- package/dist-pkg/platform/reading-in-worker.d.ts +64 -0
- package/dist-pkg/platform/reading-in-worker.js +125 -0
- package/dist-pkg/platform/reading-model.d.ts +94 -0
- package/dist-pkg/platform/reading-model.js +272 -0
- package/dist-pkg/platform/reading-runtime.d.ts +24 -0
- package/dist-pkg/platform/reading-runtime.js +164 -0
- package/dist-pkg/platform/reading-worker.d.ts +41 -0
- package/dist-pkg/platform/reading-worker.js +58 -0
- package/dist-pkg/platform/reading.d.ts +80 -0
- package/dist-pkg/platform/reading.js +136 -0
- package/dist-pkg/platform/speech-recognition.d.ts +60 -0
- package/dist-pkg/platform/speech-recognition.js +104 -0
- package/dist-pkg/platform/speech.d.ts +23 -1
- package/dist-pkg/platform/speech.js +29 -18
- package/dist-pkg/platform/storage-keys.d.ts +96 -0
- package/dist-pkg/platform/storage-keys.js +106 -0
- package/dist-pkg/platform/storage.d.ts +46 -111
- package/dist-pkg/platform/storage.js +104 -169
- package/dist-pkg/platform/tts.d.ts +85 -39
- package/dist-pkg/platform/tts.js +447 -127
- package/dist-pkg/platform/vision-loop.d.ts +23 -0
- package/dist-pkg/platform/vision-loop.js +56 -0
- package/dist-pkg/platform/vision.d.ts +132 -0
- package/dist-pkg/platform/vision.js +117 -0
- package/dist-pkg/platform/voice-listener.d.ts +40 -0
- package/dist-pkg/platform/voice-listener.js +88 -0
- package/dist-pkg/platform/voice-plan.d.ts +15 -98
- package/dist-pkg/platform/voice-plan.js +56 -143
- package/dist-pkg/platform/vosk-runtime.d.ts +96 -0
- package/dist-pkg/platform/vosk-runtime.js +151 -0
- package/dist-pkg/platform/vosk-vocabulary.d.ts +6 -0
- package/dist-pkg/platform/vosk-vocabulary.js +178 -0
- package/dist-pkg/render/canvas.d.ts +13 -7
- package/dist-pkg/render/canvas.js +13 -13
- package/dist-pkg/render/crt.d.ts +48 -12
- package/dist-pkg/render/crt.js +78 -80
- package/dist-pkg/render/cvd-matrices.d.ts +22 -18
- package/dist-pkg/render/cvd-matrices.js +37 -38
- package/dist-pkg/render/hc-role-data.d.ts +7 -7
- package/dist-pkg/render/hc-role-data.js +14 -14
- package/dist-pkg/render/high-contrast.d.ts +67 -56
- package/dist-pkg/render/high-contrast.js +226 -217
- package/dist-pkg/render/low-vision-drawing.d.ts +5 -0
- package/dist-pkg/render/low-vision-drawing.js +46 -0
- package/dist-pkg/render/lq-filter.d.ts +32 -20
- package/dist-pkg/render/lq-filter.js +59 -65
- package/dist-pkg/render/port.d.ts +86 -88
- package/dist-pkg/render/port.js +17 -23
- package/dist-pkg/render/screen-pipeline.d.ts +42 -42
- package/dist-pkg/render/screen-pipeline.js +80 -87
- package/dist-pkg/render/sprite-fx.d.ts +2 -1
- package/dist-pkg/render/sprite-fx.js +16 -14
- package/dist-pkg/render/viewports.d.ts +20 -15
- package/dist-pkg/render/viewports.js +83 -114
- package/dist-pkg/render/viz-axes-labels.d.ts +47 -0
- package/dist-pkg/render/viz-axes-labels.js +88 -0
- package/dist-pkg/render/viz-axes.d.ts +89 -86
- package/dist-pkg/render/viz-axes.js +128 -116
- package/dist-pkg/render/viz-modes.d.ts +28 -29
- package/dist-pkg/render/viz-modes.js +40 -45
- package/dist-pkg/render/viz-refusal.d.ts +32 -0
- package/dist-pkg/render/viz-refusal.js +57 -0
- package/dist-pkg/render/viz-setters.d.ts +104 -66
- package/dist-pkg/render/viz-setters.js +182 -191
- package/dist-pkg/ui/aac-sets.d.ts +45 -0
- package/dist-pkg/ui/aac-sets.js +41 -0
- package/dist-pkg/ui/audio-choices.d.ts +68 -0
- package/dist-pkg/ui/audio-choices.js +98 -0
- package/dist-pkg/ui/audio-rows-that-apply.d.ts +12 -0
- package/dist-pkg/ui/audio-rows-that-apply.js +43 -0
- package/dist-pkg/ui/camera-control.d.ts +53 -0
- package/dist-pkg/ui/camera-control.js +48 -0
- package/dist-pkg/ui/changed-mark.d.ts +9 -8
- package/dist-pkg/ui/changed-mark.js +22 -53
- package/dist-pkg/ui/control-choices.d.ts +37 -0
- package/dist-pkg/ui/control-choices.js +84 -0
- package/dist-pkg/ui/debug-panel.d.ts +59 -43
- package/dist-pkg/ui/debug-panel.js +68 -69
- package/dist-pkg/ui/declared-words.d.ts +21 -0
- package/dist-pkg/ui/declared-words.js +66 -0
- package/dist-pkg/ui/dom.d.ts +10 -14
- package/dist-pkg/ui/dom.js +10 -47
- package/dist-pkg/ui/drawing-problems.d.ts +25 -0
- package/dist-pkg/ui/drawing-problems.js +94 -0
- package/dist-pkg/ui/eye-control.d.ts +36 -0
- package/dist-pkg/ui/eye-control.js +178 -0
- package/dist-pkg/ui/face-control.d.ts +25 -0
- package/dist-pkg/ui/face-control.js +183 -0
- package/dist-pkg/ui/focus-trap.d.ts +39 -49
- package/dist-pkg/ui/focus-trap.js +56 -65
- package/dist-pkg/ui/fonts.d.ts +157 -68
- package/dist-pkg/ui/fonts.js +305 -115
- package/dist-pkg/ui/footer-scroll-driver.d.ts +14 -0
- package/dist-pkg/ui/footer-scroll-driver.js +201 -0
- package/dist-pkg/ui/footer-scroll.d.ts +38 -0
- package/dist-pkg/ui/footer-scroll.js +55 -0
- package/dist-pkg/ui/game-options.d.ts +47 -0
- package/dist-pkg/ui/game-options.js +158 -0
- package/dist-pkg/ui/gaze-overlay.d.ts +64 -0
- package/dist-pkg/ui/gaze-overlay.js +165 -0
- package/dist-pkg/ui/hand-control.d.ts +25 -0
- package/dist-pkg/ui/hand-control.js +143 -0
- package/dist-pkg/ui/help-panel.d.ts +87 -0
- package/dist-pkg/ui/help-panel.js +242 -0
- package/dist-pkg/ui/hud-bands.d.ts +62 -0
- package/dist-pkg/ui/hud-bands.js +146 -0
- package/dist-pkg/ui/hud-row.d.ts +34 -0
- package/dist-pkg/ui/hud-row.js +80 -0
- package/dist-pkg/ui/hud.d.ts +73 -73
- package/dist-pkg/ui/hud.js +94 -98
- package/dist-pkg/ui/item-announcement.d.ts +13 -12
- package/dist-pkg/ui/item-announcement.js +25 -26
- package/dist-pkg/ui/layout.d.ts +103 -28
- package/dist-pkg/ui/layout.js +117 -99
- package/dist-pkg/ui/libras-avatar-clip.d.ts +30 -0
- package/dist-pkg/ui/libras-avatar-clip.js +86 -0
- package/dist-pkg/ui/libras-avatar-load.d.ts +52 -0
- package/dist-pkg/ui/libras-avatar-load.js +134 -0
- package/dist-pkg/ui/libras-avatar-plan.d.ts +102 -0
- package/dist-pkg/ui/libras-avatar-plan.js +211 -0
- package/dist-pkg/ui/libras-avatar-player.d.ts +28 -0
- package/dist-pkg/ui/libras-avatar-player.js +226 -0
- package/dist-pkg/ui/libras-avatar-stage.d.ts +37 -0
- package/dist-pkg/ui/libras-avatar-stage.js +208 -0
- package/dist-pkg/ui/libras-glosses.d.ts +28 -0
- package/dist-pkg/ui/libras-glosses.js +161 -0
- package/dist-pkg/ui/locale-flags.d.ts +16 -0
- package/dist-pkg/ui/locale-flags.js +30 -0
- package/dist-pkg/ui/loop-crash.d.ts +17 -17
- package/dist-pkg/ui/loop-crash.js +33 -62
- package/dist-pkg/ui/menu-intent.d.ts +61 -0
- package/dist-pkg/ui/menu-intent.js +88 -0
- package/dist-pkg/ui/menu-items.d.ts +20 -0
- package/dist-pkg/ui/menu-items.js +51 -0
- package/dist-pkg/ui/menu-nav.d.ts +98 -92
- package/dist-pkg/ui/menu-nav.js +398 -286
- package/dist-pkg/ui/mobility-choices.d.ts +26 -0
- package/dist-pkg/ui/mobility-choices.js +41 -0
- package/dist-pkg/ui/motion-choices.d.ts +31 -0
- package/dist-pkg/ui/motion-choices.js +68 -0
- package/dist-pkg/ui/motion-scene.d.ts +24 -19
- package/dist-pkg/ui/motion-scene.js +29 -61
- package/dist-pkg/ui/mount-panel.d.ts +81 -0
- package/dist-pkg/ui/mount-panel.js +61 -0
- package/dist-pkg/ui/panel-shell.d.ts +65 -31
- package/dist-pkg/ui/panel-shell.js +92 -66
- package/dist-pkg/ui/panel-widgets.d.ts +150 -0
- package/dist-pkg/ui/panel-widgets.js +294 -0
- package/dist-pkg/ui/pause-buttons.d.ts +35 -0
- package/dist-pkg/ui/pause-buttons.js +66 -0
- package/dist-pkg/ui/pause-icons.d.ts +261 -347
- package/dist-pkg/ui/pause-icons.js +671 -601
- package/dist-pkg/ui/pause-markup.d.ts +86 -0
- package/dist-pkg/ui/pause-markup.js +125 -0
- package/dist-pkg/ui/reach-notice.d.ts +22 -23
- package/dist-pkg/ui/reach-notice.js +55 -58
- package/dist-pkg/ui/scan-overlay.d.ts +23 -0
- package/dist-pkg/ui/scan-overlay.js +50 -0
- package/dist-pkg/ui/screen-text.d.ts +25 -0
- package/dist-pkg/ui/screen-text.js +152 -0
- package/dist-pkg/ui/session-clock.d.ts +32 -0
- package/dist-pkg/ui/session-clock.js +67 -0
- package/dist-pkg/ui/settings-aac.d.ts +70 -0
- package/dist-pkg/ui/settings-aac.js +208 -0
- package/dist-pkg/ui/settings-audio.d.ts +97 -114
- package/dist-pkg/ui/settings-audio.js +493 -425
- package/dist-pkg/ui/settings-controls.d.ts +53 -96
- package/dist-pkg/ui/settings-controls.js +143 -215
- package/dist-pkg/ui/settings-empathy.d.ts +39 -30
- package/dist-pkg/ui/settings-empathy.js +53 -60
- package/dist-pkg/ui/settings-mobility.d.ts +192 -0
- package/dist-pkg/ui/settings-mobility.js +332 -0
- package/dist-pkg/ui/settings-motion.d.ts +91 -70
- package/dist-pkg/ui/settings-motion.js +287 -198
- package/dist-pkg/ui/settings-panel.d.ts +58 -45
- package/dist-pkg/ui/settings-panel.js +189 -94
- package/dist-pkg/ui/settings-typo.d.ts +29 -78
- package/dist-pkg/ui/settings-typo.js +158 -129
- package/dist-pkg/ui/settings-visual.d.ts +29 -87
- package/dist-pkg/ui/settings-visual.js +240 -193
- package/dist-pkg/ui/shell.d.ts +112 -135
- package/dist-pkg/ui/shell.js +148 -217
- package/dist-pkg/ui/simulation-list.d.ts +27 -0
- package/dist-pkg/ui/simulation-list.js +76 -0
- package/dist-pkg/ui/simulation-over-the-world.d.ts +23 -0
- package/dist-pkg/ui/simulation-over-the-world.js +131 -0
- package/dist-pkg/ui/simulation-refusal.d.ts +5 -31
- package/dist-pkg/ui/simulation-refusal.js +8 -56
- package/dist-pkg/ui/switchable-control.d.ts +11 -0
- package/dist-pkg/ui/switchable-control.js +19 -0
- package/dist-pkg/ui/title.d.ts +3 -12
- package/dist-pkg/ui/title.js +49 -18
- package/dist-pkg/ui/top-band.d.ts +26 -0
- package/dist-pkg/ui/top-band.js +96 -0
- package/dist-pkg/ui/typo-choices.d.ts +71 -0
- package/dist-pkg/ui/typo-choices.js +85 -0
- package/dist-pkg/ui/visual-axes-panel.d.ts +11 -47
- package/dist-pkg/ui/visual-axes-panel.js +12 -94
- package/dist-pkg/ui/visual-choices.d.ts +77 -0
- package/dist-pkg/ui/visual-choices.js +107 -0
- package/dist-pkg/ui/vlibras.d.ts +77 -10
- package/dist-pkg/ui/vlibras.js +130 -81
- package/dist-pkg/ui/voice-control.d.ts +68 -0
- package/dist-pkg/ui/voice-control.js +276 -0
- package/dist-pkg/ui/voice-settings.d.ts +95 -0
- package/dist-pkg/ui/voice-settings.js +352 -0
- package/dist-pkg/ui/where-the-child-is.d.ts +31 -0
- package/dist-pkg/ui/where-the-child-is.js +70 -0
- package/docs/CREDITS.md +300 -44
- package/docs/LICENSES.md +189 -145
- package/package.json +51 -27
- package/scripts/check-cartridge.mjs +78 -0
- package/scripts/game-build.d.mts +25 -0
- package/scripts/game-build.mjs +144 -0
- package/scripts/heavy-into-the-delivery.mjs +388 -0
- package/scripts/libras-avatar.json +680 -0
- package/scripts/libras-avatar.mjs +246 -0
- package/scripts/libras-glosses/gloss.py +95 -0
- package/scripts/libras-glosses/pyproject.toml +32 -0
- package/scripts/libras-glosses/uv.lock +781 -0
- package/scripts/libras-glosses.mjs +351 -0
- package/scripts/licences/Apache-2.0.txt +176 -0
- package/scripts/licences/GPL-3.0.txt +674 -0
- package/scripts/licences/MIT.txt +17 -0
- package/scripts/licences/OFL-1.1.txt +86 -0
- package/scripts/licences/UFL-1.0.txt +96 -0
- package/scripts/licences/fonts.mjs +389 -0
- package/scripts/licences/third-party.mjs +301 -0
- package/scripts/licences/vosk-browser.NOTICE.txt +268 -0
- package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
- package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
- package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-400.woff2 +0 -0
- package/app/public/vendor/fonts/inter-700.woff2 +0 -0
- package/app/public/vendor/fonts/lato-400.woff2 +0 -0
- package/app/public/vendor/fonts/lato-700.woff2 +0 -0
- package/app/public/vendor/fonts/literata-400.woff2 +0 -0
- package/app/public/vendor/fonts/literata-700.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
- package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
- package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
- package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
- package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
- package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
- package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
- package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
- package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
- package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
- package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
- package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
- package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
- package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
- package/dist-pkg/core/anel.d.ts +0 -17
- package/dist-pkg/core/anel.js +0 -31
- package/dist-pkg/core/collision.d.ts +0 -21
- package/dist-pkg/core/collision.js +0 -62
- package/dist-pkg/core/layers.d.ts +0 -63
- package/dist-pkg/core/layers.js +0 -95
- package/dist-pkg/core/letter-grid.d.ts +0 -55
- package/dist-pkg/core/letter-grid.js +0 -116
- package/dist-pkg/core/password.d.ts +0 -34
- package/dist-pkg/core/password.js +0 -205
- package/dist-pkg/core/rotulo-acessivel.d.ts +0 -17
- package/dist-pkg/core/rotulo-acessivel.js +0 -41
- package/dist-pkg/core/run-state.d.ts +0 -115
- package/dist-pkg/core/run-state.js +0 -40
- package/dist-pkg/core/tiles.d.ts +0 -12
- package/dist-pkg/core/tiles.js +0 -61
- package/dist-pkg/core/world.d.ts +0 -4
- package/dist-pkg/core/world.js +0 -58
- package/dist-pkg/input/origem-sintetica.d.ts +0 -44
- package/dist-pkg/input/origem-sintetica.js +0 -60
- package/dist-pkg/input/transporte-em-uso.d.ts +0 -101
- package/dist-pkg/input/transporte-em-uso.js +0 -130
- package/dist-pkg/platform/audio-nav.d.ts +0 -51
- package/dist-pkg/platform/audio-nav.js +0 -99
- package/dist-pkg/platform/guide-intensity.d.ts +0 -40
- package/dist-pkg/platform/guide-intensity.js +0 -73
- package/dist-pkg/platform/pesados-catalogo.d.ts +0 -14
- package/dist-pkg/platform/pesados-catalogo.js +0 -177
- package/dist-pkg/platform/pesados.d.ts +0 -31
- package/dist-pkg/platform/pesados.js +0 -75
- package/dist-pkg/platform/vozes-prontas.d.ts +0 -28
- package/dist-pkg/platform/vozes-prontas.js +0 -61
- package/dist-pkg/render/camera.d.ts +0 -58
- package/dist-pkg/render/camera.js +0 -105
- package/dist-pkg/render/cenario-data.d.ts +0 -127
- package/dist-pkg/render/cenario-data.js +0 -145
- package/dist-pkg/render/city-tex.d.ts +0 -63
- package/dist-pkg/render/city-tex.js +0 -228
- package/dist-pkg/render/city-tiles.d.ts +0 -11
- package/dist-pkg/render/city-tiles.js +0 -140
- package/dist-pkg/render/draw.d.ts +0 -149
- package/dist-pkg/render/draw.js +0 -231
- package/dist-pkg/render/fx.d.ts +0 -63
- package/dist-pkg/render/fx.js +0 -123
- package/dist-pkg/render/minimap.d.ts +0 -11
- package/dist-pkg/render/minimap.js +0 -94
- package/dist-pkg/render/parallax.d.ts +0 -85
- package/dist-pkg/render/parallax.js +0 -158
- package/dist-pkg/render/player-anim.d.ts +0 -71
- package/dist-pkg/render/player-anim.js +0 -152
- package/dist-pkg/render/recycling-tex.d.ts +0 -48
- package/dist-pkg/render/recycling-tex.js +0 -164
- package/dist-pkg/render/scene-city.d.ts +0 -77
- package/dist-pkg/render/scene-city.js +0 -181
- package/dist-pkg/render/scene-parallax.d.ts +0 -72
- package/dist-pkg/render/scene-parallax.js +0 -426
- package/dist-pkg/render/scene-sky.d.ts +0 -126
- package/dist-pkg/render/scene-sky.js +0 -295
- package/dist-pkg/render/set-cenario.d.ts +0 -54
- package/dist-pkg/render/set-cenario.js +0 -103
- package/dist-pkg/render/textures.d.ts +0 -75
- package/dist-pkg/render/textures.js +0 -240
- package/dist-pkg/render/title-scene.d.ts +0 -46
- package/dist-pkg/render/title-scene.js +0 -75
- package/dist-pkg/render/weather.d.ts +0 -96
- package/dist-pkg/render/weather.js +0 -171
- package/dist-pkg/render/wheelchair-sprites.d.ts +0 -32
- package/dist-pkg/render/wheelchair-sprites.js +0 -69
- package/dist-pkg/render/world-tex.d.ts +0 -18
- package/dist-pkg/render/world-tex.js +0 -87
- package/dist-pkg/ui/activities-menu.d.ts +0 -258
- package/dist-pkg/ui/activities-menu.js +0 -648
- package/dist-pkg/ui/caa-sets.d.ts +0 -39
- package/dist-pkg/ui/caa-sets.js +0 -65
- package/dist-pkg/ui/latch-refusal.d.ts +0 -32
- package/dist-pkg/ui/latch-refusal.js +0 -60
- package/dist-pkg/ui/map-hub.d.ts +0 -56
- package/dist-pkg/ui/map-hub.js +0 -138
- package/dist-pkg/ui/settings-caa.d.ts +0 -45
- package/dist-pkg/ui/settings-caa.js +0 -137
- package/dist-pkg/ui/settings-motor.d.ts +0 -175
- package/dist-pkg/ui/settings-motor.js +0 -342
- package/dist-pkg/ui/webcam.d.ts +0 -7
- package/dist-pkg/ui/webcam.js +0 -93
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/reading-runtime — THE READING, RUN (ADR-0216 §2; issues #185, #200): sound in, the child's words out.
|
|
3
|
+
//
|
|
4
|
+
// The model of the child's language is opened from the delivery (`heavy/` on the page's own origin, ADR-0177) and run here:
|
|
5
|
+
// the encoder once, then the decoder token by token until it says it is done. What each graph is called, what it takes and what
|
|
6
|
+
// comes back is not guessed — `scratchpad/leitura-de-referencia-3.py` ran the real files and its trace is what
|
|
7
|
+
// `tests/reading-runtime.node.test.js` replays.
|
|
8
|
+
//
|
|
9
|
+
// THREE FAMILIES, and they differ in ways that matter:
|
|
10
|
+
// · Whisper (pt) eats a log-mel spectrogram and opens with four tokens — «transcribe, in Portuguese, without timestamps» — and
|
|
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, in whole frames of the length its config names, and opens with one token, in two decoder
|
|
13
|
+
// graphs like Whisper's.
|
|
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
|
|
15
|
+
// caches on the first. It is the shape most easily written wrong from a guess, which is why it is traced too.
|
|
16
|
+
//
|
|
17
|
+
// ⚠️ THE CROSS-ATTENTION CACHE IS COMPUTED ONCE AND NEVER AGAIN. The first pass answers a `present.*.encoder.*` per layer that
|
|
18
|
+
// depends only on the sound; feeding back the new one each step is how a loop gets slower with every token AND drifts.
|
|
19
|
+
import { atDelivery, loadOnnxRuntime } from './onnx-runtime.js';
|
|
20
|
+
import { inWholeFrames, logMel, nextToken, readingModelFor, readingTextOf, suppressedTokens, tokenIdOf, waveFrameOf, WHISPER_BANDS, WHISPER_FRAMES, } from './reading-model.js';
|
|
21
|
+
const MAX_TOKENS = 200;
|
|
22
|
+
export async function loadReadingRuntime(d) {
|
|
23
|
+
const plan = readingModelFor(d.language);
|
|
24
|
+
if (!plan)
|
|
25
|
+
throw new Error(`reading: the project has no model for ${d.language}`);
|
|
26
|
+
const get = d.fetch;
|
|
27
|
+
const ort = d.ort ?? await loadOnnxRuntime({ base: d.base, importModule: d.importModule });
|
|
28
|
+
const asJson = async (id) => {
|
|
29
|
+
const r = await get(atDelivery(id, d.base));
|
|
30
|
+
if (!r.ok)
|
|
31
|
+
throw new Error(`reading: HTTP ${r.status} for ${id} — the delivery does not carry this language's model`);
|
|
32
|
+
return await r.json();
|
|
33
|
+
};
|
|
34
|
+
const asSession = async (id) => {
|
|
35
|
+
const r = await get(atDelivery(id, d.base));
|
|
36
|
+
if (!r.ok)
|
|
37
|
+
throw new Error(`reading: HTTP ${r.status} for ${id} — the delivery does not carry this language's model`);
|
|
38
|
+
return ort.InferenceSession.create(new Uint8Array(await r.arrayBuffer()), { executionProviders: ['wasm'] });
|
|
39
|
+
};
|
|
40
|
+
const [tokenizer, generation, config] = await Promise.all([
|
|
41
|
+
asJson(plan.tokenizer),
|
|
42
|
+
asJson(plan.generation),
|
|
43
|
+
asJson(plan.config),
|
|
44
|
+
]);
|
|
45
|
+
const melFilters = plan.input === 'log-mel' && plan.preprocessor
|
|
46
|
+
? (await asJson(plan.preprocessor)).mel_filters
|
|
47
|
+
: null;
|
|
48
|
+
const [encoder, decoder, decoderPast] = await Promise.all([
|
|
49
|
+
asSession(plan.encoder),
|
|
50
|
+
asSession(plan.decoder),
|
|
51
|
+
plan.decoderPast ? asSession(plan.decoderPast) : Promise.resolve(null),
|
|
52
|
+
]);
|
|
53
|
+
const start = openingOf(plan, tokenizer, generation);
|
|
54
|
+
const eos = generation.eos_token_id ?? -1;
|
|
55
|
+
const { heads, width } = cacheSizeOf(config);
|
|
56
|
+
const ceiling = d.maxTokens ?? MAX_TOKENS;
|
|
57
|
+
const frame = waveFrameOf(config);
|
|
58
|
+
return {
|
|
59
|
+
async transcribe(samples) {
|
|
60
|
+
const encoded = await encoder.run(soundFor(ort, plan, samples, melFilters, frame));
|
|
61
|
+
const hidden = encoded[encoder.outputNames[0]];
|
|
62
|
+
const shape = {
|
|
63
|
+
ort, start, hidden, heads, width,
|
|
64
|
+
frames: hidden.dims?.[1] ?? 0,
|
|
65
|
+
// the merged graph wants the caches present and EMPTY on the first pass, and the flag that says which pass this is
|
|
66
|
+
merged: !decoderPast,
|
|
67
|
+
};
|
|
68
|
+
const past = {};
|
|
69
|
+
const ids = [];
|
|
70
|
+
for (let step = 0; step < ceiling; step++) {
|
|
71
|
+
const first = step === 0;
|
|
72
|
+
const session = first || shape.merged ? decoder : decoderPast;
|
|
73
|
+
const answered = await session.run(stepInputs(shape, session, first, ids, past));
|
|
74
|
+
keepPast(past, session, answered, first);
|
|
75
|
+
const chosen = nextToken(lastLogits(answered[session.outputNames[0]]), suppressedTokens(generation, first));
|
|
76
|
+
// no token at all (a row of NaN) is an end, like the end-of-text: pushing it would ask for a word the vocabulary lacks
|
|
77
|
+
if (chosen < 0 || chosen === eos)
|
|
78
|
+
break;
|
|
79
|
+
ids.push(chosen);
|
|
80
|
+
}
|
|
81
|
+
return readingTextOf(tokenizer, ids).trim();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
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
|
|
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.
|
|
89
|
+
*/
|
|
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]) };
|
|
96
|
+
}
|
|
97
|
+
/** What one decoder step is handed: the opening on the first, then only the newest token, and the caches so far. */
|
|
98
|
+
function stepInputs(shape, session, first, ids, past) {
|
|
99
|
+
const { ort } = shape;
|
|
100
|
+
const inputIds = first ? shape.start : [ids[ids.length - 1]];
|
|
101
|
+
const inputs = {
|
|
102
|
+
input_ids: new ort.Tensor('int64', BigInt64Array.from(inputIds, BigInt), [1, inputIds.length]),
|
|
103
|
+
encoder_hidden_states: shape.hidden,
|
|
104
|
+
...(first && shape.merged ? emptyCaches(ort, session, shape.heads, shape.width, shape.frames) : past),
|
|
105
|
+
};
|
|
106
|
+
if (shape.merged)
|
|
107
|
+
inputs.use_cache_branch = new ort.Tensor('bool', Uint8Array.of(first ? 0 : 1), [1]);
|
|
108
|
+
return inputs;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The caches carried to the next step: each `present.*` the decoder answered, renamed to the `past_key_values.*` it takes. The
|
|
112
|
+
* `logits` beside them are the answer, never a cache.
|
|
113
|
+
* ⚠️ ONLY the self-attention half grows. The cross-attention half is the sound, and the sound does not change.
|
|
114
|
+
*/
|
|
115
|
+
function keepPast(past, session, answered, first) {
|
|
116
|
+
for (const name of session.outputNames) {
|
|
117
|
+
if (!name.startsWith('present.'))
|
|
118
|
+
continue;
|
|
119
|
+
if (first || name.includes('.decoder.'))
|
|
120
|
+
past[name.replace('present.', 'past_key_values.')] = answered[name];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* HOW THE DECODER IS TOLD WHAT TO DO. Moonshine opens with one token and reads what it hears. Whisper opens with four, and they
|
|
125
|
+
* are an instruction: transcribe (not translate), in THIS language, without timestamps. 📏 Measured: without them it answers in
|
|
126
|
+
* whatever language it decides, and with `<|translate|>` it answers in English — a child reading Portuguese would be corrected
|
|
127
|
+
* against an English transcript.
|
|
128
|
+
*/
|
|
129
|
+
function openingOf(plan, tokenizer, generation) {
|
|
130
|
+
const first = generation.decoder_start_token_id ?? 0;
|
|
131
|
+
if (plan.input !== 'log-mel')
|
|
132
|
+
return [first];
|
|
133
|
+
const named = [`<|${plan.language}|>`, '<|transcribe|>', '<|notimestamps|>'].map((c) => tokenIdOf(tokenizer, c));
|
|
134
|
+
if (named.some((id) => id === null))
|
|
135
|
+
throw new Error(`reading: this model has no token for ${plan.language} transcription`);
|
|
136
|
+
return [first, ...named];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* How big one cache is, from `config.json`: the KEY-VALUE heads (a model with grouped attention keeps fewer than it attends with),
|
|
140
|
+
* and a width of `hidden_size / heads`, 64 where the config does not say.
|
|
141
|
+
*/
|
|
142
|
+
function cacheSizeOf(config) {
|
|
143
|
+
return {
|
|
144
|
+
heads: config.num_key_value_heads ?? config.num_attention_heads ?? 0,
|
|
145
|
+
width: config.hidden_size && config.num_attention_heads ? config.hidden_size / config.num_attention_heads : 64,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** The caches a merged decoder wants on its first pass: the self-attention halves empty, the cross-attention ones the sound's. */
|
|
149
|
+
function emptyCaches(ort, session, heads, width, frames) {
|
|
150
|
+
const out = {};
|
|
151
|
+
for (const name of session.inputNames ?? []) {
|
|
152
|
+
if (!name.startsWith('past_key_values.'))
|
|
153
|
+
continue;
|
|
154
|
+
const length = name.includes('.decoder.') ? 0 : frames;
|
|
155
|
+
out[name] = new ort.Tensor('float32', new Float32Array(heads * length * width), [1, heads, length, width]);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
/** The logits of the LAST position: the decoder answers one row per token it was given, and only the newest one is a choice. */
|
|
160
|
+
function lastLogits(tensor) {
|
|
161
|
+
const t = tensor;
|
|
162
|
+
const vocab = t.dims?.[2] ?? t.data.length;
|
|
163
|
+
return t.data.subarray(t.data.length - vocab);
|
|
164
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import { loadReadingRuntime } from './reading-runtime.js';
|
|
3
|
+
/** What the client asks for. */
|
|
4
|
+
export type ReadingRequest = {
|
|
5
|
+
readonly kind: 'load';
|
|
6
|
+
readonly base: string;
|
|
7
|
+
readonly language: string;
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: 'transcribe';
|
|
10
|
+
readonly id: number;
|
|
11
|
+
readonly samples: Float32Array;
|
|
12
|
+
};
|
|
13
|
+
/** What the worker answers. `id` is absent only on the answer to `load`, which nobody can ask twice. */
|
|
14
|
+
export type ReadingAnswer = {
|
|
15
|
+
readonly kind: 'ready';
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: 'text';
|
|
18
|
+
readonly id: number;
|
|
19
|
+
readonly text: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: 'failed';
|
|
22
|
+
readonly id?: number;
|
|
23
|
+
readonly message: string;
|
|
24
|
+
};
|
|
25
|
+
/** The side of the conversation this file needs, so a gate can run the whole protocol without a thread. */
|
|
26
|
+
export interface WorkerScope {
|
|
27
|
+
onmessage: ((event: {
|
|
28
|
+
data: ReadingRequest;
|
|
29
|
+
}) => void) | null;
|
|
30
|
+
postMessage(answer: ReadingAnswer): void;
|
|
31
|
+
/**
|
|
32
|
+
* The worker realm's own `fetch`, which the model's files are fetched with (ADR-0232 D4): the runtime receives it from the
|
|
33
|
+
* scope, so the worker's entry reaches no bare global either.
|
|
34
|
+
*/
|
|
35
|
+
fetch(url: string): Promise<Response>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Wires the protocol onto a scope. Exported — and not only run at import — so the gate exercises the REAL handler instead of a
|
|
39
|
+
* copy of it written in the test, which is the difference between measuring the protocol and measuring a description of it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function serveReading(scope: WorkerScope, load?: typeof loadReadingRuntime): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/reading-worker — THE TRANSCRIPTION, OFF THE MAIN THREAD (ADR-0216 §2; issue #185).
|
|
3
|
+
//
|
|
4
|
+
// 🔴 WHY A THREAD OF ITS OWN, and it is measured and not hygiene: `platform/reading-runtime` runs an encoder and then a decoder
|
|
5
|
+
// token by token, each one a wasm call of tens of milliseconds. On the main thread that is the thread that also draws the game
|
|
6
|
+
// and feeds the microphone — 📏 measured in the lab, Whisper transcribing on the main thread CUT THE AUDIO IN GAPS OF 4 s; the
|
|
7
|
+
// same work in a worker left a biggest gap of 264 ms. A child who reads aloud while the page transcribes loses the words she
|
|
8
|
+
// said during the pause, and nothing anywhere says so.
|
|
9
|
+
//
|
|
10
|
+
// The protocol is three messages and no state the caller cannot see:
|
|
11
|
+
// · `{ kind: 'load', base, language }` → `{ kind: 'ready' }` or `{ kind: 'failed', message }`
|
|
12
|
+
// · `{ kind: 'transcribe', id, samples }` → `{ kind: 'text', id, text }` or `{ kind: 'failed', id, message }`
|
|
13
|
+
// Every answer carries the `id` of the question, because a second reading may be asked for while the first is still running and
|
|
14
|
+
// an answer that cannot be matched to its question is an answer given to the wrong child.
|
|
15
|
+
//
|
|
16
|
+
// ⚠️ THE MODEL IS OPENED HERE, INSIDE THE WORKER, and that is the whole point: `loadReadingRuntime` fetches the graphs from the
|
|
17
|
+
// delivery and the ONNX runtime compiles them. Doing that on the main thread freezes the game for as long as it takes to open
|
|
18
|
+
// 378 MiB of model, which is the other half of the defect this file exists to remove.
|
|
19
|
+
import { loadReadingRuntime } from './reading-runtime.js';
|
|
20
|
+
const reason = (e) => (e instanceof Error ? e.message : String(e));
|
|
21
|
+
/**
|
|
22
|
+
* Wires the protocol onto a scope. Exported — and not only run at import — so the gate exercises the REAL handler instead of a
|
|
23
|
+
* copy of it written in the test, which is the difference between measuring the protocol and measuring a description of it.
|
|
24
|
+
*/
|
|
25
|
+
export function serveReading(scope, load = loadReadingRuntime) {
|
|
26
|
+
let transcriber = null;
|
|
27
|
+
scope.onmessage = (event) => {
|
|
28
|
+
const msg = event.data;
|
|
29
|
+
if (msg.kind === 'load') {
|
|
30
|
+
void (async () => {
|
|
31
|
+
try {
|
|
32
|
+
// a METHOD call on the scope: `fetch` detached from its realm throws «Illegal invocation»
|
|
33
|
+
transcriber = await load({ base: msg.base, language: msg.language, fetch: (url) => scope.fetch(url) });
|
|
34
|
+
scope.postMessage({ kind: 'ready' });
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
scope.postMessage({ kind: 'failed', message: reason(e) });
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// ⚠️ A REQUEST BEFORE THE MODEL IS OPEN IS ANSWERED, not dropped: a dropped message leaves the caller's promise pending for
|
|
43
|
+
// ever, and a child waiting for words that will never come has no way to know she should try again.
|
|
44
|
+
void (async () => {
|
|
45
|
+
try {
|
|
46
|
+
if (!transcriber)
|
|
47
|
+
throw new Error('reading worker: asked to transcribe before the model was opened');
|
|
48
|
+
scope.postMessage({ kind: 'text', id: msg.id, text: await transcriber.transcribe(msg.samples) });
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
scope.postMessage({ kind: 'failed', id: msg.id, message: reason(e) });
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (typeof WorkerGlobalScope !== 'undefined' && typeof self !== 'undefined' && self instanceof WorkerGlobalScope) {
|
|
57
|
+
serveReading(self);
|
|
58
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import { recognitionRoute, type RecognitionApi, type OnDeviceAvailability, type RecognitionInstance } from './speech-recognition.js';
|
|
3
|
+
import type { VoiceChooser, ChooseSettings, ChoiceHeard } from './choose-by-voice.js';
|
|
4
|
+
/** The browser's recognition object, with the parts a reading uses. Assigned handlers, as the API has them. */
|
|
5
|
+
export interface ListeningSession extends RecognitionInstance {
|
|
6
|
+
start(): void;
|
|
7
|
+
stop(): void;
|
|
8
|
+
onresult: ((ev: {
|
|
9
|
+
readonly results: ArrayLike<ArrayLike<{
|
|
10
|
+
readonly transcript: string;
|
|
11
|
+
}> & {
|
|
12
|
+
readonly isFinal: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
}) => void) | null;
|
|
15
|
+
onerror: ((ev: {
|
|
16
|
+
readonly error?: string;
|
|
17
|
+
}) => void) | null;
|
|
18
|
+
onend: (() => void) | null;
|
|
19
|
+
}
|
|
20
|
+
/** What a reading gives back. `route` is which recogniser heard it — for `problems` and for a lab, never for the game's logic. */
|
|
21
|
+
export interface Heard {
|
|
22
|
+
readonly text: string;
|
|
23
|
+
readonly route: 'webspeech-local' | 'model';
|
|
24
|
+
/** The reading stopped because the child went quiet, because the time ran out, or because someone asked it to stop. */
|
|
25
|
+
readonly ended: 'silence' | 'timeout' | 'asked';
|
|
26
|
+
}
|
|
27
|
+
export interface ReadingDeps {
|
|
28
|
+
/** The BCP-47 tag of the child's language (`core/i18n.bcp47`). */
|
|
29
|
+
readonly language: () => string;
|
|
30
|
+
/** The browser's `SpeechRecognition` constructor, or nothing where there is none. */
|
|
31
|
+
readonly api?: RecognitionApi | null;
|
|
32
|
+
readonly now: () => number;
|
|
33
|
+
readonly every: (fn: () => void, ms: number) => unknown;
|
|
34
|
+
readonly stopEvery: (h: unknown) => void;
|
|
35
|
+
/** A line for `problems`, written once per reason (ADR-0169). */
|
|
36
|
+
readonly report: (line: string) => void;
|
|
37
|
+
/**
|
|
38
|
+
* The engine's own recogniser, when there is one to load — the models of ADR-0201, lazily (`platform/reading-runtime`).
|
|
39
|
+
* Absent: the refusal below.
|
|
40
|
+
*/
|
|
41
|
+
readonly model?: (language: string) => Promise<{
|
|
42
|
+
transcribe(samples: Float32Array): Promise<string>;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* The microphone, as 16 kHz mono samples. The model route needs sound, and the browser route does not: the browser's own
|
|
46
|
+
* recogniser opens the microphone itself. ⚠️ Absent with a model present is a reading that REFUSES and says which half is
|
|
47
|
+
* missing — never one that answers an empty sentence, which a game would show a child as «you read nothing».
|
|
48
|
+
*/
|
|
49
|
+
readonly record?: (options: ListenOptions) => Promise<Float32Array>;
|
|
50
|
+
readonly route?: typeof recognitionRoute;
|
|
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;
|
|
57
|
+
}
|
|
58
|
+
export interface ListenOptions {
|
|
59
|
+
/** How long a silence ends the reading. Default 1.5 s: shorter cuts a child who is thinking between words. */
|
|
60
|
+
readonly silenceMs?: number;
|
|
61
|
+
/** The ceiling, so a forgotten microphone does not listen for ever. Default 30 s, the window Whisper reads at once. */
|
|
62
|
+
readonly maxMs?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface Reading {
|
|
65
|
+
/** Listens until the child goes quiet, and answers with what was read. */
|
|
66
|
+
listen(options?: ListenOptions): Promise<Heard>;
|
|
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. */
|
|
73
|
+
stop(): void;
|
|
74
|
+
/** Can this device serve the child's language at all? Answers with the reason when it cannot. */
|
|
75
|
+
ready(): Promise<{
|
|
76
|
+
readonly can: boolean;
|
|
77
|
+
readonly why: OnDeviceAvailability | 'sem-api' | 'sem-processamento-local' | 'no-model';
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
export declare function createReading(d: ReadingDeps): Reading;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/reading — THE CHILD READS ALOUD, AND THE GAME RECEIVES TEXT (ADR-0216, issue #200).
|
|
3
|
+
//
|
|
4
|
+
// The Dev, 2026-09-21: «a engine deve oferecer objetos de leitura e TTS. O jogo não deve precisar saber como isso funciona, apenas
|
|
5
|
+
// deve pedir para ouvir e receber o texto.» So this module answers ONE question — what did the child say — and keeps the rest:
|
|
6
|
+
// which recogniser heard it, in which language, and whether it may run at all.
|
|
7
|
+
//
|
|
8
|
+
// · ON THE DEVICE OR NOT AT ALL (ADR-0200 erratum). The browser's recogniser is used only where it says it recognises on the device
|
|
9
|
+
// AND the language is installed there (`platform/speech-recognition`). Anything else waits for the models the project carries
|
|
10
|
+
// (Whisper for pt, Moonshine for en and es — ADR-0201), which arrive through `engine` below and are not built yet: until then a
|
|
11
|
+
// request is REFUSED with its reason, never answered by a recogniser that would send a child's voice to a server.
|
|
12
|
+
// · A reading ends when the child stops talking (`silenceMs`) or when the time runs out (`maxMs`), whichever comes first, and what
|
|
13
|
+
// was heard so far is what the game gets — a child who stops mid-sentence is not an error.
|
|
14
|
+
// · Asking twice does not open two microphones: the second caller waits on the same reading.
|
|
15
|
+
import { recognitionRoute, createOnDeviceRecognition } from './speech-recognition.js';
|
|
16
|
+
const SILENCE_MS = 1500;
|
|
17
|
+
const MAX_MS = 30_000;
|
|
18
|
+
export function createReading(d) {
|
|
19
|
+
const routeOf = d.route ?? recognitionRoute;
|
|
20
|
+
const make = d.createSession ?? ((api, language) => createOnDeviceRecognition(api, language));
|
|
21
|
+
const said = new Set();
|
|
22
|
+
const once = (key, line) => { if (!said.has(key)) {
|
|
23
|
+
said.add(key);
|
|
24
|
+
d.report(line);
|
|
25
|
+
} };
|
|
26
|
+
let inFlight = null;
|
|
27
|
+
let askToStop = null;
|
|
28
|
+
async function ready() {
|
|
29
|
+
const { route: route, status: state } = await routeOf(d.language(), d.api);
|
|
30
|
+
if (route === 'webspeech-local')
|
|
31
|
+
return { can: true, why: state };
|
|
32
|
+
return { can: !!d.model, why: d.model ? state : 'no-model' };
|
|
33
|
+
}
|
|
34
|
+
function byBrowser(api, options) {
|
|
35
|
+
const silenceMs = options.silenceMs ?? SILENCE_MS;
|
|
36
|
+
const maxMs = options.maxMs ?? MAX_MS;
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let session;
|
|
39
|
+
try {
|
|
40
|
+
session = make(api, d.language());
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let text = '', lastSound = d.now(), done = false;
|
|
47
|
+
const ticker = d.every(() => {
|
|
48
|
+
if (done)
|
|
49
|
+
return;
|
|
50
|
+
const quietFor = d.now() - lastSound;
|
|
51
|
+
if (quietFor >= silenceMs && text)
|
|
52
|
+
finish('silence');
|
|
53
|
+
else if (d.now() - startedAt >= maxMs)
|
|
54
|
+
finish('timeout');
|
|
55
|
+
}, 100);
|
|
56
|
+
const startedAt = d.now();
|
|
57
|
+
const finish = (ended) => {
|
|
58
|
+
if (done)
|
|
59
|
+
return;
|
|
60
|
+
done = true;
|
|
61
|
+
d.stopEvery(ticker);
|
|
62
|
+
askToStop = null;
|
|
63
|
+
try {
|
|
64
|
+
session.stop();
|
|
65
|
+
}
|
|
66
|
+
catch { /* a session that already ended is not a failure */ }
|
|
67
|
+
resolve({ text: text.trim(), route: 'webspeech-local', ended });
|
|
68
|
+
};
|
|
69
|
+
askToStop = () => finish('asked');
|
|
70
|
+
session.onresult = (ev) => {
|
|
71
|
+
let together = '';
|
|
72
|
+
for (let i = 0; i < ev.results.length; i++)
|
|
73
|
+
together += `${ev.results[i][0].transcript} `;
|
|
74
|
+
text = together;
|
|
75
|
+
lastSound = d.now();
|
|
76
|
+
};
|
|
77
|
+
session.onerror = (ev) => {
|
|
78
|
+
// ⚠️ A refusal is not a silence: a child waiting to be heard must not wait for ever, and the adult must read why.
|
|
79
|
+
once('error', `reading: the browser's recogniser stopped (${ev.error ?? 'unknown'}) — the child reads and nothing is heard; `
|
|
80
|
+
+ 'allow the microphone for this page, or install the language on the device');
|
|
81
|
+
if (!done) {
|
|
82
|
+
done = true;
|
|
83
|
+
d.stopEvery(ticker);
|
|
84
|
+
askToStop = null;
|
|
85
|
+
reject(new Error(ev.error ?? 'recognition failed'));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
session.onend = () => { if (!done)
|
|
89
|
+
finish(text ? 'silence' : 'asked'); };
|
|
90
|
+
session.start();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
ready,
|
|
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
|
+
},
|
|
104
|
+
async listen(options = {}) {
|
|
105
|
+
if (inFlight)
|
|
106
|
+
return inFlight; // one microphone, one reading: the second caller waits on the first
|
|
107
|
+
const { route: route, status: state } = await routeOf(d.language(), d.api);
|
|
108
|
+
if (route !== 'webspeech-local') {
|
|
109
|
+
if (!d.model) {
|
|
110
|
+
once('sem-route', `reading: nothing on this device can hear ${d.language()} (${state}) — the child's reading is not assessed; `
|
|
111
|
+
+ 'install the language in the browser, or build a delivery that carries the reading model for this language');
|
|
112
|
+
throw new Error(`reading is not available here: ${state}`);
|
|
113
|
+
}
|
|
114
|
+
if (!d.record) {
|
|
115
|
+
once('sem-microfone', `reading: the model for ${d.language()} is here and nothing captures the child's voice — `
|
|
116
|
+
+ 'the reading cannot run; pass `record`, the 16 kHz samples the model reads');
|
|
117
|
+
throw new Error('reading has no microphone here');
|
|
118
|
+
}
|
|
119
|
+
const capture = d.record;
|
|
120
|
+
inFlight = (async () => {
|
|
121
|
+
const [transcriber, samples] = await Promise.all([d.model(d.language()), capture(options)]);
|
|
122
|
+
return { text: (await transcriber.transcribe(samples)).trim(), route: 'model', ended: 'silence' };
|
|
123
|
+
})();
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
inFlight = byBrowser(d.api, options);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return await inFlight;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
inFlight = null;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/** What the browser answers about recognising a language on the device (Chrome's `SpeechRecognition.available`). */
|
|
3
|
+
export type OnDeviceAvailability = 'available' | 'downloadable' | 'downloading' | 'unavailable';
|
|
4
|
+
/** The part of the browser's `SpeechRecognition` constructor this module reads. */
|
|
5
|
+
export interface RecognitionApi {
|
|
6
|
+
new (): RecognitionInstance;
|
|
7
|
+
readonly prototype: object;
|
|
8
|
+
readonly available?: (options: {
|
|
9
|
+
langs: readonly string[];
|
|
10
|
+
processLocally: boolean;
|
|
11
|
+
}) => Promise<OnDeviceAvailability>;
|
|
12
|
+
}
|
|
13
|
+
export interface RecognitionInstance {
|
|
14
|
+
lang: string;
|
|
15
|
+
processLocally?: boolean;
|
|
16
|
+
continuous: boolean;
|
|
17
|
+
interimResults: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface RecognitionRoute {
|
|
20
|
+
/** `webspeech-local`: the browser recognises on the device. `recuo`: the engine's own recogniser. */
|
|
21
|
+
readonly route: 'webspeech-local' | 'recuo';
|
|
22
|
+
/** Why: the browser's answer, or what is missing. */
|
|
23
|
+
readonly status: OnDeviceAvailability | 'sem-api' | 'sem-processamento-local';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* THE ROUTE, measured on the device (ADR-0200 §3). ⚠️ A browser whose recognition object has no `processLocally` would take the
|
|
27
|
+
* flag as an unknown property and recognise on its servers — it is never the route, whatever it says about availability.
|
|
28
|
+
*/
|
|
29
|
+
export declare function recognitionRoute(language: string, api: RecognitionApi | null | undefined): Promise<RecognitionRoute>;
|
|
30
|
+
/** A recognition object set to recognise on the device, continuously, with partial hypotheses. Refuses a browser that cannot. */
|
|
31
|
+
export declare function createOnDeviceRecognition(api: RecognitionApi, language: string): RecognitionInstance;
|
|
32
|
+
/** A command heard: a direction word, or the name of an item of the open menu. */
|
|
33
|
+
export type HeardCommand = {
|
|
34
|
+
readonly kind: 'palavra';
|
|
35
|
+
readonly word: string;
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: 'item';
|
|
38
|
+
readonly name: string;
|
|
39
|
+
};
|
|
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 declare const spokenText: (t: string) => string;
|
|
46
|
+
export interface CommandReader {
|
|
47
|
+
/** The menu's item names now on screen (already as spoken). */
|
|
48
|
+
items(names: readonly string[]): void;
|
|
49
|
+
/**
|
|
50
|
+
* One hypothesis of utterance `index`: returns the commands it completes that were not returned before for that utterance. A
|
|
51
|
+
* partial one holds back its last name while another item's name continues it («voltar» may become «voltar ao jogo»).
|
|
52
|
+
*/
|
|
53
|
+
read(index: number, text: string, final: boolean): readonly HeardCommand[];
|
|
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;
|
|
60
|
+
export declare function createCommandReader(vocabulary: readonly string[]): CommandReader;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/speech-recognition — which recogniser hears the child, and what a command is in what was heard (ADR-0200 and its
|
|
3
|
+
// erratum, ADR-0193, ADR-0194; issue #190).
|
|
4
|
+
//
|
|
5
|
+
// The browser's recognition comes first, and ONLY on the device: the Dev, «Só local, voz da criança não deve sair do aparelho». So
|
|
6
|
+
// Web Speech is the route only where the browser can say it recognises on the device (`processLocally`) AND the language is
|
|
7
|
+
// installed there; anything else — no API, a browser that would ignore the flag and send the voice to its servers, a language to
|
|
8
|
+
// download — falls back to the recognisers the engine carries (Vosk for commands). The words heard, whoever heard them, become
|
|
9
|
+
// commands the same way: the direction words and the open menu's item names, the longest name first, a partial hypothesis firing
|
|
10
|
+
// what it already holds and a name another item's name continues waiting for the end (ADR-0194 §3).
|
|
11
|
+
/**
|
|
12
|
+
* THE ROUTE, measured on the device (ADR-0200 §3). ⚠️ A browser whose recognition object has no `processLocally` would take the
|
|
13
|
+
* flag as an unknown property and recognise on its servers — it is never the route, whatever it says about availability.
|
|
14
|
+
*/
|
|
15
|
+
export async function recognitionRoute(language, api) {
|
|
16
|
+
if (!api)
|
|
17
|
+
return { route: 'recuo', status: 'sem-api' };
|
|
18
|
+
if (!('processLocally' in api.prototype) || typeof api.available !== 'function')
|
|
19
|
+
return { route: 'recuo', status: 'sem-processamento-local' };
|
|
20
|
+
let availability;
|
|
21
|
+
try {
|
|
22
|
+
availability = await api.available({ langs: [language], processLocally: true });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { route: 'recuo', status: 'unavailable' };
|
|
26
|
+
}
|
|
27
|
+
return { route: availability === 'available' ? 'webspeech-local' : 'recuo', status: availability };
|
|
28
|
+
}
|
|
29
|
+
/** A recognition object set to recognise on the device, continuously, with partial hypotheses. Refuses a browser that cannot. */
|
|
30
|
+
export function createOnDeviceRecognition(api, language) {
|
|
31
|
+
if (!('processLocally' in api.prototype))
|
|
32
|
+
throw new Error('speech recognition on the device is not supported here: the voice would leave the device');
|
|
33
|
+
const rec = new api();
|
|
34
|
+
rec.lang = language;
|
|
35
|
+
rec.processLocally = true;
|
|
36
|
+
rec.continuous = true;
|
|
37
|
+
rec.interimResults = true;
|
|
38
|
+
return rec;
|
|
39
|
+
}
|
|
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
|
+
}
|
|
61
|
+
export function createCommandReader(vocabulary) {
|
|
62
|
+
const words = byKey(vocabulary);
|
|
63
|
+
let items = new Map();
|
|
64
|
+
const firedCount = new Map();
|
|
65
|
+
const phrasesIn = (text) => {
|
|
66
|
+
const p = compareKey(text).split(' ').filter(Boolean);
|
|
67
|
+
const found = [];
|
|
68
|
+
for (let i = 0; i < p.length;) {
|
|
69
|
+
let matched = null;
|
|
70
|
+
for (let n = p.length - i; n >= 1; n--) {
|
|
71
|
+
const f = p.slice(i, i + n).join(' ');
|
|
72
|
+
if (items.has(f) || words.has(f)) {
|
|
73
|
+
matched = f;
|
|
74
|
+
i += n;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (matched)
|
|
79
|
+
found.push(matched);
|
|
80
|
+
else
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
return found;
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
items(names) { items = byKey(names); },
|
|
87
|
+
read(index, text, final) {
|
|
88
|
+
const found = phrasesIn(text);
|
|
89
|
+
const alreadyFired = firedCount.get(index) ?? 0;
|
|
90
|
+
const newCommands = [];
|
|
91
|
+
for (let i = alreadyFired; i < found.length; i++) {
|
|
92
|
+
const f = found[i];
|
|
93
|
+
if (!final && i === found.length - 1 && [...items.keys()].some((o) => o !== f && o.startsWith(f + ' ')))
|
|
94
|
+
break;
|
|
95
|
+
const item = items.get(f);
|
|
96
|
+
newCommands.push(item !== undefined ? { kind: 'item', name: item } : { kind: 'palavra', word: words.get(f) });
|
|
97
|
+
firedCount.set(index, i + 1);
|
|
98
|
+
}
|
|
99
|
+
if (final)
|
|
100
|
+
firedCount.delete(index);
|
|
101
|
+
return newCommands;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|