@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,52 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/kokoro-runtime — THE ENGINE LOADS THE NEURAL VOICE ITSELF (ADR-0216 and its erratum; 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». So the
|
|
5
|
+
// phonemizer and the ONNX runtime are no longer a game's dependency: they are catalogued heavy files (`voz:runtime:*`), the build
|
|
6
|
+
// puts them in the delivery, and this loads them from `heavy/` on the page's own origin — the same path the vision runtime
|
|
7
|
+
// already takes (ADR-0177, `platform/vision`).
|
|
8
|
+
//
|
|
9
|
+
// ⚠️ NOTHING HERE IS IMPORTED FROM npm, and that is the point: an `import 'espeak-ng'` would put the phonemizer in the bundle of
|
|
10
|
+
// every game that merely links the engine, and would need a bundler to resolve it. `import(url)` of an address on our own origin
|
|
11
|
+
// needs neither, and it happens only when a child picks a neural voice.
|
|
12
|
+
//
|
|
13
|
+
// · The wasm of espeak-ng is compiled ONCE and instantiated per sentence: its module runs one program and exits.
|
|
14
|
+
// · onnxruntime's threads load their own glue and wasm; they are pointed at `heavy/` too, because a worker that goes looking for
|
|
15
|
+
// them on a CDN finds nothing in a school with no network, and the child hears silence (measured in the quiz demo, #181).
|
|
16
|
+
import { createKokoroPort } from './kokoro-port.js';
|
|
17
|
+
import { atDelivery, loadOnnxRuntime } from './onnx-runtime.js';
|
|
18
|
+
// The addresses are the catalogue's, reached by id through `platform/onnx-runtime` — never written twice (ADR-0114, ADR-0177).
|
|
19
|
+
const ESPEAK_GLUE = 'voz:runtime:fonemas';
|
|
20
|
+
const ESPEAK_WASM = 'voz:runtime:fonemas:wasm';
|
|
21
|
+
/**
|
|
22
|
+
* Builds the neural voice. Everything it needs is fetched from the delivery, so a device that never had a network day has it all
|
|
23
|
+
* or has none of it — and «none of it» is a refusal the caller reports, not a silence.
|
|
24
|
+
*/
|
|
25
|
+
export async function loadKokoroRuntime(d) {
|
|
26
|
+
const { compileWasm: compile, instantiateWasm: instantiate, fetch: get } = d;
|
|
27
|
+
const loadModule = d.importModule ?? ((url) => import(/* @vite-ignore */ url));
|
|
28
|
+
const espeakModule = await loadModule(atDelivery(ESPEAK_GLUE, d.base));
|
|
29
|
+
const factory = (typeof espeakModule === 'function' ? espeakModule : espeakModule.default);
|
|
30
|
+
if (typeof factory !== 'function')
|
|
31
|
+
throw new Error('Kokoro: espeak-ng did not load from the delivery (voz:runtime:fonemas)');
|
|
32
|
+
// by BYTES and not by stream: a server that sends the file without `application/wasm` must not break the voice
|
|
33
|
+
let compiled = null;
|
|
34
|
+
const espeak = (options) => {
|
|
35
|
+
compiled ??= get(atDelivery(ESPEAK_WASM, d.base)).then(async (r) => {
|
|
36
|
+
if (!r.ok)
|
|
37
|
+
throw new Error(`Kokoro: HTTP ${r.status} for espeak-ng's wasm — the delivery does not carry it`);
|
|
38
|
+
return compile(await r.arrayBuffer());
|
|
39
|
+
});
|
|
40
|
+
const module = compiled;
|
|
41
|
+
return factory({
|
|
42
|
+
...options,
|
|
43
|
+
instantiateWasm: (imports, ready) => {
|
|
44
|
+
void module.then(async (m) => ready(await instantiate(m, imports), m));
|
|
45
|
+
return {};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
// the runtime, and the rule about its threads, are `platform/onnx-runtime`'s — the reading loads the same one the same way
|
|
50
|
+
const ort = await loadOnnxRuntime({ base: d.base, importModule: loadModule });
|
|
51
|
+
return createKokoroPort({ espeak, ort, fetch: get, base: d.base });
|
|
52
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import type { NeuralVoice } from './voice-plan.js';
|
|
3
|
+
/** A Kokoro inference session on one device: token ids and a style row in, a 24 kHz waveform out. */
|
|
4
|
+
export interface KokoroSession {
|
|
5
|
+
readonly synthesize: (ids: readonly number[], styleVector: Float32Array) => Promise<Float32Array>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* WHAT SPEAKING NEEDS OF KOKORO — `platform/kokoro-runtime` fills it from the delivery (ADR-0216 §1).
|
|
9
|
+
* `phonemize` is espeak-ng in the voice's language (`pt-br`, `es-419`, `en-us`, `en-gb`); `vocabulary` the model tokenizer's symbols;
|
|
10
|
+
* `voice` a voice's style table; `session` a session on WebGPU or WASM.
|
|
11
|
+
*
|
|
12
|
+
* 📌 IT LIVES IN THIS LEAF and not beside the speaking, so that the runtime that builds one can be reached from `platform/tts` by
|
|
13
|
+
* `import()` without the two naming each other — a cycle the direction gate (ADR-0173) refuses, and rightly: the loader must not
|
|
14
|
+
* have to load the speaker to be loaded by it.
|
|
15
|
+
*/
|
|
16
|
+
export interface KokoroModule {
|
|
17
|
+
readonly phonemize: (text: string, espeak: string) => Promise<string>;
|
|
18
|
+
readonly vocabulary: () => Promise<Readonly<{
|
|
19
|
+
[symbol: string]: number;
|
|
20
|
+
}>>;
|
|
21
|
+
readonly voice: (id: string) => Promise<Float32Array>;
|
|
22
|
+
readonly session: (device: 'webgpu' | 'wasm') => Promise<KokoroSession>;
|
|
23
|
+
}
|
|
24
|
+
/** How the neural voice arrives. Until ADR-0216 every game wrote one of these; now the engine has its own. */
|
|
25
|
+
export type LoadKokoro = () => Promise<KokoroModule>;
|
|
26
|
+
/** A Kokoro voice of the engine's three languages. `recommended` marks the two the Dev rated good (ADR-0198 §3). */
|
|
27
|
+
export interface KokoroVoice extends NeuralVoice {
|
|
28
|
+
readonly engine: 'kokoro';
|
|
29
|
+
/** The espeak-ng voice that phonemizes this voice's text. */
|
|
30
|
+
readonly espeak: string;
|
|
31
|
+
readonly recommended: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Kokoro-82M v1.0's voices for Portuguese, Spanish and English, as the model repository lists them (read 2026-09-14). The model's
|
|
35
|
+
* first letter is the language — `p` Brazilian Portuguese, `e` Spanish, `a` American and `b` British English — and the second the
|
|
36
|
+
* voice's gender. The others (French, Hindi, Italian, Japanese, Chinese) are not the engine's languages.
|
|
37
|
+
*/
|
|
38
|
+
export declare const KOKORO_VOICES: readonly KokoroVoice[];
|
|
39
|
+
export declare const KOKORO_MODEL_URL = "https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX/resolve/main/onnx/model.onnx";
|
|
40
|
+
export declare const URL_DO_TOKENIZADOR_KOKORO = "https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX/resolve/main/tokenizer.json";
|
|
41
|
+
export declare const kokoroVoiceUrl: (id: string) => string;
|
|
42
|
+
/**
|
|
43
|
+
* Each voice file's SHA-256 and size, read from the repository's LFS metadata on 2026-09-14 (all 522 240 bytes); `pf_dora` also
|
|
44
|
+
* measured on the downloaded file. The model: 325 532 232 bytes, measured on the file. Checked before anything is kept (#168).
|
|
45
|
+
*/
|
|
46
|
+
export declare const KOKORO_VOICES_SHA256: Readonly<{
|
|
47
|
+
[id: string]: string;
|
|
48
|
+
}>;
|
|
49
|
+
export declare const KOKORO_VOICE_BYTES = 522240;
|
|
50
|
+
export declare const KOKORO_MODEL_SHA256 = "8fbea51ea711f2af382e88c833d9e288c6dc82ce5e98421ea61c058ce21a34cb";
|
|
51
|
+
export declare const KOKORO_MODEL_BYTES = 325532232;
|
|
52
|
+
export declare const SHA256_DO_TOKENIZADOR_KOKORO = "77a02c8e164413299b4b4c403b14f8e0e1c1b727db4d46a09d6327b861060a34";
|
|
53
|
+
export declare const BYTES_DO_TOKENIZADOR_KOKORO = 3497;
|
|
54
|
+
/** The model's context: ids between the two pad tokens. A longer sentence is cut here (the caller splits text by sentence). */
|
|
55
|
+
export declare const MAX_KOKORO_TOKENS = 510;
|
|
56
|
+
/** A voice file is a table of style rows, one per token count, 256 numbers each. */
|
|
57
|
+
export declare const STYLE_DIMENSION = 256;
|
|
58
|
+
/** The token ids of a phoneme string: each symbol the vocabulary knows, in order, between two pad tokens (id 0). */
|
|
59
|
+
export declare function tokenize(phonemes: string, symbolIds: Readonly<{
|
|
60
|
+
[symbol: string]: number;
|
|
61
|
+
}>): number[];
|
|
62
|
+
/** The style row a sentence of `tokens` ids (pads excluded) takes from a voice table. */
|
|
63
|
+
export declare function sentenceStyle(styleTable: Float32Array, tokens: number): Float32Array;
|
|
64
|
+
/**
|
|
65
|
+
* Is a synthesis speech? Measured on 2026-09-14: WebGPU on one AMD GPU gave a waveform whose samples ran to 2×10⁷ — noise a child
|
|
66
|
+
* would hear as a burst. A speech waveform stays within ±1 (a sample or two past it is clipping, not noise) and is not silent.
|
|
67
|
+
*/
|
|
68
|
+
export declare function eFala(waveform: ArrayLike<number>): boolean;
|
|
69
|
+
/** A 16-bit mono PCM WAV of a waveform, clipped to ±1 — what the narration's media element plays. */
|
|
70
|
+
export declare function wavDe(waveform: ArrayLike<number>, sampleRate?: number): ArrayBuffer;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
const voiceEntry = (id, locale, espeak, isRecommended = false) => Object.freeze({ locale, engine: 'kokoro', voice: id, espeak, recommended: isRecommended });
|
|
3
|
+
/**
|
|
4
|
+
* Kokoro-82M v1.0's voices for Portuguese, Spanish and English, as the model repository lists them (read 2026-09-14). The model's
|
|
5
|
+
* first letter is the language — `p` Brazilian Portuguese, `e` Spanish, `a` American and `b` British English — and the second the
|
|
6
|
+
* voice's gender. The others (French, Hindi, Italian, Japanese, Chinese) are not the engine's languages.
|
|
7
|
+
*/
|
|
8
|
+
export const KOKORO_VOICES = Object.freeze([
|
|
9
|
+
voiceEntry('pf_dora', 'pt-BR', 'pt-br'), voiceEntry('pm_alex', 'pt-BR', 'pt-br'), voiceEntry('pm_santa', 'pt-BR', 'pt-br'),
|
|
10
|
+
voiceEntry('ef_dora', 'es', 'es-419'), voiceEntry('em_alex', 'es', 'es-419'), voiceEntry('em_santa', 'es', 'es-419'),
|
|
11
|
+
voiceEntry('af_heart', 'en-US', 'en-us', true), voiceEntry('af_bella', 'en-US', 'en-us', true),
|
|
12
|
+
...['af_alloy', 'af_aoede', 'af_jessica', 'af_kore', 'af_nicole', 'af_nova', 'af_river', 'af_sarah', 'af_sky',
|
|
13
|
+
'am_adam', 'am_echo', 'am_eric', 'am_fenrir', 'am_liam', 'am_michael', 'am_onyx', 'am_puck', 'am_santa'].map((id) => voiceEntry(id, 'en-US', 'en-us')),
|
|
14
|
+
...['bf_alice', 'bf_emma', 'bf_isabella', 'bf_lily', 'bm_daniel', 'bm_fable', 'bm_george', 'bm_lewis'].map((id) => voiceEntry(id, 'en-GB', 'en-gb')),
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* WHERE KOKORO COMES FROM, in one place (ADR-0114's rule for hosts): the model repository the catalogue fetches at build time; the
|
|
18
|
+
* delivery serves the same paths from its own origin (ADR-0177), and a game's port asks for them there.
|
|
19
|
+
*/
|
|
20
|
+
const KOKORO_REPOSITORY = 'https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX/resolve/main';
|
|
21
|
+
export const KOKORO_MODEL_URL = `${KOKORO_REPOSITORY}/onnx/model.onnx`;
|
|
22
|
+
export const URL_DO_TOKENIZADOR_KOKORO = `${KOKORO_REPOSITORY}/tokenizer.json`;
|
|
23
|
+
export const kokoroVoiceUrl = (id) => `${KOKORO_REPOSITORY}/voices/${id}.bin`;
|
|
24
|
+
/**
|
|
25
|
+
* Each voice file's SHA-256 and size, read from the repository's LFS metadata on 2026-09-14 (all 522 240 bytes); `pf_dora` also
|
|
26
|
+
* measured on the downloaded file. The model: 325 532 232 bytes, measured on the file. Checked before anything is kept (#168).
|
|
27
|
+
*/
|
|
28
|
+
export const KOKORO_VOICES_SHA256 = Object.freeze({
|
|
29
|
+
pf_dora: '3da7b5b2d91847ebf5646f57631af6ececae3c29a89cd300f06edf9aa6cfe9ee',
|
|
30
|
+
pm_alex: '0175c753f59c54e7fd5a995bedef0c5ff2fb67e0043dd3dcb2ae74ec2acbeb2a',
|
|
31
|
+
pm_santa: '8b012db3185778afe2e45a62cbad69db73021774fe68dda634bcc748a982eede',
|
|
32
|
+
ef_dora: 'f66ec66bd295acb18372e37008533a9a3228483ccd294e7538d5d9294ac9a532',
|
|
33
|
+
em_alex: '27809e9eafdcbcfff90a3016c697568676531de2a2c39cee29c96c7bd6b83e95',
|
|
34
|
+
em_santa: 'ad43b774e1ca24d05c6161297d8aeb770ac3d29bb95daf516727af5f7d543683',
|
|
35
|
+
af_heart: 'd583ccff3cdca2f7fae535cb998ac07e9fcb90f09737b9a41fa2734ec44a8f0b',
|
|
36
|
+
af_bella: 'f69d836209b78eb8c66e75e3cda491e26ea838a3674257e9d4e5703cbaf55c8b',
|
|
37
|
+
af_alloy: 'c4a6b876047fd7fb472edf4ebd63cfac7c3b958a7cae7c106e8f038ca6308c45',
|
|
38
|
+
af_aoede: '4a004c33430762e2461eedb2013fad808ef4ab3121f5300f554476caf58d8361',
|
|
39
|
+
af_jessica: 'a240a5e3c15b43563d6e923bdca8ef5613a23471d9b77653694012435df23bd8',
|
|
40
|
+
af_kore: '9be5221b6a941c04b561959b8ff0b06e809444dcc4ab7e75a7b23606f691819e',
|
|
41
|
+
af_nicole: 'cd2191ab31b914ed7b318416b0e4440fdf392ddad9106a060819aa600a64f59a',
|
|
42
|
+
af_nova: '18778272caa0d0eebaea251c35fd635f038434f9eee5e691d02a174bd328414f',
|
|
43
|
+
af_river: '00a2bcf82b1d86e8f19902ede58c65ccf6c0e43b44b7d74fad54e5d8933c9c30',
|
|
44
|
+
af_sarah: '4409fbc125afabacc615d94db5398d847006a737b0247d6892b7a9a0007a2f0a',
|
|
45
|
+
af_sky: '4435255c9744f3f31659e0d714ab7689bf65d9e77ec1cce060f083912614f0b9',
|
|
46
|
+
am_adam: '162b035ed91cfc48b6046982184c645f72edcdd1b82843347f605d7bf7b15716',
|
|
47
|
+
am_echo: '3968b92c3c4cd1c4416dbded36c13eaa388a90d5788d02a13e4d781f5f8cf3c3',
|
|
48
|
+
am_eric: 'e8b5be17edd1e3636901ce7598baafe2dc8dd8ff707a0c23bf9e461add7e2832',
|
|
49
|
+
am_fenrir: 'c27989f741f7ee34d273a39d8a595cc0837d35f5ced9a29b7cc162614616df43',
|
|
50
|
+
am_liam: '52403be32fd047c6a44517cb0bcd6b134f2a18baa73e70ef41651e0eab921ade',
|
|
51
|
+
am_michael: '1d1f21dd8da39c30705cd4c75d039d265e9bc4a2a93ed09bc9e1b1225eb95ba1',
|
|
52
|
+
am_onyx: 'da5d135b424164916d75a68ffb4c2abce3d7d5ccc82dd1ee6cf447ce286145e6',
|
|
53
|
+
am_puck: 'fcf73c989033e9233e0b98713eca600c8c74dcc1614b37009d5450ff4a2274a0',
|
|
54
|
+
am_santa: '61150cf726ab6c5ed7a99f90a304f91f5a72c00c592e89ec94e5df11c319227a',
|
|
55
|
+
bf_alice: '08afa6ba24da61ea5e8efa139e5aadc938d83f0a6da5a900adaf763ac1da5573',
|
|
56
|
+
bf_emma: '669fe0647f9dd04fcab92f1439a40eeb4c8b4ab1f82e4996fe3d918ce4a63b73',
|
|
57
|
+
bf_isabella: '3754352c4aaa46d17f27654ab7518d65b62ad6163a0f55a5f4330c2da2c4e94f',
|
|
58
|
+
bf_lily: '5e0ee32ebe64a467124976b14e69590746f1c4ce41a12b587a50c862edfea335',
|
|
59
|
+
bm_daniel: '6b3194bbceffb746733cbc22c8f593dd44e401a71d53895a2dca891bc595a1e8',
|
|
60
|
+
bm_fable: 'f889083196807b4adb15e9204252165f503b8d33d3982e681c52443c49d798f1',
|
|
61
|
+
bm_george: 'c4b235a4c1f2cd3b939fed08b899ce9385638b763f7b73a59616c4fc9bd6c9bc',
|
|
62
|
+
bm_lewis: 'b8f671cef828c30e66fdf0b0756a76bba58f6bb3398cbbf27058642acbcedb97',
|
|
63
|
+
});
|
|
64
|
+
export const KOKORO_VOICE_BYTES = 522_240;
|
|
65
|
+
export const KOKORO_MODEL_SHA256 = '8fbea51ea711f2af382e88c833d9e288c6dc82ce5e98421ea61c058ce21a34cb';
|
|
66
|
+
export const KOKORO_MODEL_BYTES = 325_532_232;
|
|
67
|
+
export const SHA256_DO_TOKENIZADOR_KOKORO = '77a02c8e164413299b4b4c403b14f8e0e1c1b727db4d46a09d6327b861060a34';
|
|
68
|
+
export const BYTES_DO_TOKENIZADOR_KOKORO = 3_497;
|
|
69
|
+
/** The model's context: ids between the two pad tokens. A longer sentence is cut here (the caller splits text by sentence). */
|
|
70
|
+
export const MAX_KOKORO_TOKENS = 510;
|
|
71
|
+
/** A voice file is a table of style rows, one per token count, 256 numbers each. */
|
|
72
|
+
export const STYLE_DIMENSION = 256;
|
|
73
|
+
const KOKORO_SAMPLE_RATE = 24_000;
|
|
74
|
+
/** The token ids of a phoneme string: each symbol the vocabulary knows, in order, between two pad tokens (id 0). */
|
|
75
|
+
export function tokenize(phonemes, symbolIds) {
|
|
76
|
+
const ids = [];
|
|
77
|
+
for (const symbol of phonemes) {
|
|
78
|
+
const id = symbolIds[symbol];
|
|
79
|
+
if (id !== undefined)
|
|
80
|
+
ids.push(id);
|
|
81
|
+
if (ids.length === MAX_KOKORO_TOKENS)
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return [0, ...ids, 0];
|
|
85
|
+
}
|
|
86
|
+
/** The style row a sentence of `tokens` ids (pads excluded) takes from a voice table. */
|
|
87
|
+
export function sentenceStyle(styleTable, tokens) {
|
|
88
|
+
const rowCount = Math.floor(styleTable.length / STYLE_DIMENSION);
|
|
89
|
+
const row = Math.max(0, Math.min(rowCount - 1, tokens));
|
|
90
|
+
return styleTable.slice(row * STYLE_DIMENSION, (row + 1) * STYLE_DIMENSION);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Is a synthesis speech? Measured on 2026-09-14: WebGPU on one AMD GPU gave a waveform whose samples ran to 2×10⁷ — noise a child
|
|
94
|
+
* would hear as a burst. A speech waveform stays within ±1 (a sample or two past it is clipping, not noise) and is not silent.
|
|
95
|
+
*/
|
|
96
|
+
export function eFala(waveform) {
|
|
97
|
+
if (waveform.length === 0)
|
|
98
|
+
return false;
|
|
99
|
+
let outside = 0, energy = 0;
|
|
100
|
+
for (let i = 0; i < waveform.length; i++) {
|
|
101
|
+
const s = waveform[i];
|
|
102
|
+
if (!Number.isFinite(s))
|
|
103
|
+
return false;
|
|
104
|
+
if (Math.abs(s) > 1.5)
|
|
105
|
+
outside++;
|
|
106
|
+
energy += s * s;
|
|
107
|
+
}
|
|
108
|
+
return outside === 0 && Math.sqrt(energy / waveform.length) > 1e-3;
|
|
109
|
+
}
|
|
110
|
+
/** A 16-bit mono PCM WAV of a waveform, clipped to ±1 — what the narration's media element plays. */
|
|
111
|
+
export function wavDe(waveform, sampleRate = KOKORO_SAMPLE_RATE) {
|
|
112
|
+
const n = waveform.length;
|
|
113
|
+
const buf = new ArrayBuffer(44 + n * 2);
|
|
114
|
+
const v = new DataView(buf);
|
|
115
|
+
const text = (o, s) => { for (let i = 0; i < s.length; i++)
|
|
116
|
+
v.setUint8(o + i, s.charCodeAt(i)); };
|
|
117
|
+
text(0, 'RIFF');
|
|
118
|
+
v.setUint32(4, 36 + n * 2, true);
|
|
119
|
+
text(8, 'WAVE');
|
|
120
|
+
text(12, 'fmt ');
|
|
121
|
+
v.setUint32(16, 16, true);
|
|
122
|
+
v.setUint16(20, 1, true);
|
|
123
|
+
v.setUint16(22, 1, true);
|
|
124
|
+
v.setUint32(24, sampleRate, true);
|
|
125
|
+
v.setUint32(28, sampleRate * 2, true);
|
|
126
|
+
v.setUint16(32, 2, true);
|
|
127
|
+
v.setUint16(34, 16, true);
|
|
128
|
+
text(36, 'data');
|
|
129
|
+
v.setUint32(40, n * 2, true);
|
|
130
|
+
for (let i = 0; i < n; i++)
|
|
131
|
+
v.setInt16(44 + i * 2, Math.round(Math.max(-1, Math.min(1, waveform[i])) * 32767), true);
|
|
132
|
+
return buf;
|
|
133
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/** A window that remembers what was hung on it, so it can be taken down. */
|
|
3
|
+
export interface ListenerScope {
|
|
4
|
+
/** The window to hand around. Identical to the real one, except that its listeners are remembered. */
|
|
5
|
+
readonly win: Window;
|
|
6
|
+
/**
|
|
7
|
+
* Takes off every listener still installed through `win`. Idempotent: what was already removed by hand, or removed itself
|
|
8
|
+
* (`once: true`), is simply no longer there — removing a listener that is not installed is a no-op in the DOM.
|
|
9
|
+
*/
|
|
10
|
+
releaseAll(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function createListenerScope(real: Window): ListenerScope;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/listener-scope — A WINDOW THAT CAN BE LET GO.
|
|
3
|
+
//
|
|
4
|
+
// 🔴 WHY THIS EXISTS, AND IT IS A DEFECT THAT WAS MEASURED, NOT A TIDINESS. A root (`boot/create-game`) installs about thirty
|
|
5
|
+
// listeners on the window — the scan interception, START, SELECT, `action4`, the resize, the language change, and the five that
|
|
6
|
+
// `ui/menu-nav.attach()` puts there — AND THERE WAS NO WAY TO TAKE THEM OFF. `unmount()` is not that way and must not become it:
|
|
7
|
+
// it releases the CURRENT CARTRIDGE (ADR-0142), and a root that went deaf on `unmount()` would come back from the next `mount()`
|
|
8
|
+
// with no keyboard at all.
|
|
9
|
+
//
|
|
10
|
+
// 📏 What that cost, measured in the browser with the real engine: a root whose host element was removed from the document KEEPS
|
|
11
|
+
// LISTENING, and every query it makes is document-wide (`getPauseMenu` is `doc.querySelector('#vp-pause-0')`,
|
|
12
|
+
// `topVisibleOverlay` is `doc.querySelectorAll('#game-region .overlay')`) — so the dead root finds THE LIVE ROOT'S pause card and
|
|
13
|
+
// navigates it too. One ArrowDown moved the cursor one item with one root, TWO with a second root, THREE with a third. Nobody
|
|
14
|
+
// gets an error: the child presses down once and the cursor jumps two.
|
|
15
|
+
//
|
|
16
|
+
// ⚠️ AND `stopPropagation()` SAVES NOBODY HERE. Every one of these listeners sits on the SAME node in the SAME phase, and
|
|
17
|
+
// stopping propagation does not stop the siblings on that node — only `stopImmediatePropagation()` would, and using it would mean
|
|
18
|
+
// the first root to be built silences every other one, which is a worse rule than the bug.
|
|
19
|
+
//
|
|
20
|
+
// 🎯 THE SHAPE: one seam instead of a `detach()` in each of the seven modules that listen. The root wraps the window ONCE and
|
|
21
|
+
// hands the WRAPPER down; every listener installed through it — by the root or by any module it builds — is remembered, and
|
|
22
|
+
// `releaseAll()` takes all of them off at once. A module that learns to listen tomorrow is covered without being edited, which a
|
|
23
|
+
// list of `detach()` calls could never promise: the one that is forgotten is the one that leaks.
|
|
24
|
+
//
|
|
25
|
+
// 📌 Everything else about the window is UNCHANGED. `getComputedStyle`, `requestAnimationFrame`, `matchMedia`, `innerWidth` and
|
|
26
|
+
// the rest answer exactly as the real window does — this is a window with a memory, not a window with a smaller API.
|
|
27
|
+
const captureOf = (o) => (typeof o === 'boolean' ? o : !!o?.capture);
|
|
28
|
+
export function createListenerScope(real) {
|
|
29
|
+
const installed = [];
|
|
30
|
+
// A released scope is ended: a listener armed before the end and added after it (a timer) must not reach the window.
|
|
31
|
+
let released = false;
|
|
32
|
+
const listen = (type, listener, options) => {
|
|
33
|
+
if (released)
|
|
34
|
+
return;
|
|
35
|
+
real.addEventListener(type, listener, options);
|
|
36
|
+
installed.push({ type, listener, capture: captureOf(options), options });
|
|
37
|
+
};
|
|
38
|
+
const forget = (type, listener, options) => {
|
|
39
|
+
real.removeEventListener(type, listener, options);
|
|
40
|
+
const capture = captureOf(options);
|
|
41
|
+
const i = installed.findIndex((e) => e.type === type && e.listener === listener && e.capture === capture);
|
|
42
|
+
if (i >= 0)
|
|
43
|
+
installed.splice(i, 1);
|
|
44
|
+
};
|
|
45
|
+
/*
|
|
46
|
+
* ⚠️ WHAT LEAVES THIS TRAP IS ANSWERED BY THE REAL WINDOW, NEVER BY THE PROXY, and it is not style: `innerWidth`,
|
|
47
|
+
* `devicePixelRatio` and `location` are GETTERS on the window, and `getComputedStyle`, `requestAnimationFrame` and
|
|
48
|
+
* `matchMedia` are methods of it — run any of them with the proxy as `this` and the browser answers «Illegal invocation».
|
|
49
|
+
* So the receiver is left at its default (which IS the target) and every function leaves answered by the real window.
|
|
50
|
+
*
|
|
51
|
+
* 🔴 EVERY FUNCTION, WITH NO GUESS AT WHAT IT IS (`onTheWindow`): a call runs with the real window as `this`, and everything
|
|
52
|
+
* else — `new`, the `prototype`, a static like `SpeechRecognition.available` — reaches the function itself. The rule before
|
|
53
|
+
* guessed: a function with a `prototype` was a constructor and left unbound, the rest were `bind`-ed. The guess held for the
|
|
54
|
+
* browser's own functions and failed for an extension's: one that wraps `window.fetch` does it with a plain `function`, which
|
|
55
|
+
* HAS a `prototype`, so the engine's fetch left unbound and every heavy file failed with «Illegal invocation» inside the
|
|
56
|
+
* download's `catch`. 📏 Measured on 2026-09-27 in the Dev's Brave: no heavy file ever requested, an empty checked cache, and
|
|
57
|
+
* the 👄 and the 📷 unable to start; with a wrapped fetch in a clean profile, 0 requests against 66 without it. A `bind` could
|
|
58
|
+
* not be the answer either: it drops the `prototype` that `platform/speech-recognition` reads.
|
|
59
|
+
*/
|
|
60
|
+
const answered = new WeakMap();
|
|
61
|
+
const onTheWindow = (fn) => {
|
|
62
|
+
// one wrapper per function, so the same property read twice is the same function; `new` builds with the function itself as
|
|
63
|
+
// the target — with the wrapper as `newTarget`, a constructor already bound (a host may lend one) built an object with none of
|
|
64
|
+
// its methods («observe is not a function», measured in `tests/no-need-to-hold.browser.test.js`)
|
|
65
|
+
if (!answered.has(fn))
|
|
66
|
+
answered.set(fn, new Proxy(fn, {
|
|
67
|
+
apply: (f, _this, args) => Reflect.apply(f, real, args),
|
|
68
|
+
construct: (f, args) => Reflect.construct(f, args),
|
|
69
|
+
}));
|
|
70
|
+
return answered.get(fn);
|
|
71
|
+
};
|
|
72
|
+
const win = new Proxy(real, {
|
|
73
|
+
get(target, prop) {
|
|
74
|
+
if (prop === 'addEventListener')
|
|
75
|
+
return listen;
|
|
76
|
+
if (prop === 'removeEventListener')
|
|
77
|
+
return forget;
|
|
78
|
+
const value = Reflect.get(target, prop);
|
|
79
|
+
return typeof value === 'function' ? onTheWindow(value) : value;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const releaseAll = () => {
|
|
83
|
+
released = true;
|
|
84
|
+
// Drained as it goes: a listener that took another off while being removed would otherwise make this skip one, and a scope
|
|
85
|
+
// released twice must not try to take the same listener off again.
|
|
86
|
+
for (const e of installed.splice(0))
|
|
87
|
+
real.removeEventListener(e.type, e.listener, e.options);
|
|
88
|
+
};
|
|
89
|
+
return { win, releaseAll };
|
|
90
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/** The three hooks `core/i18n`'s port takes. A host that has no page passes none, and the engine runs without one. */
|
|
3
|
+
export interface LocaleHostHooks {
|
|
4
|
+
readonly applied: (locale: string, tag: string) => void;
|
|
5
|
+
readonly preferred: () => string | null;
|
|
6
|
+
readonly follow: (react: (locale: string) => void) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* What a browser can offer a translator: the page effects of a change, the language the person's browser is set to, and
|
|
10
|
+
* the page's signal that ANOTHER root changed the language (ADR-0232 D3: the language is the page's, and every root follows).
|
|
11
|
+
*/
|
|
12
|
+
export declare function localeHostHooks(doc: Document, win: Window, applyDom: (root: ParentNode) => void): LocaleHostHooks;
|
|
13
|
+
/** Hangs a root's translator on the window for tests and the preview, which is what `core/i18n` used to do to itself. */
|
|
14
|
+
export declare function exposeI18n(win: Window, i18n: unknown): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/locale-host — WHAT A PAGE DOES WHEN THE LANGUAGE CHANGES, and what language it would prefer (ADR-0221 step 7g).
|
|
3
|
+
//
|
|
4
|
+
// 🔴 WHY THIS MODULE EXISTS: `core/i18n` used to write `document.documentElement.lang`, dispatch `i18n:change` on `window`,
|
|
5
|
+
// read `navigator.language` and hang itself on `window.__i18n` — four reaches into globals from `core`, which ADR-0173
|
|
6
|
+
// defines as «what the engine IS, without a browser». The health ratchet measured them (step 7d) and the Dev chose to SPLIT
|
|
7
|
+
// rather than to move the module (option B, 2026-09-22): the decisions — which languages exist, which one is kept, what a
|
|
8
|
+
// key resolves to — stay in `core`, and the three things only a page can do live here.
|
|
9
|
+
//
|
|
10
|
+
// 📌 WHY NOT INJECT AND LEAVE IT AT THAT, which was the cheaper option C: the sentence «core is what the engine is, without a
|
|
11
|
+
// browser» would have stayed a rule with an exception nobody reads. 📏 The cost measured before choosing was 8 sites; this
|
|
12
|
+
// module is what those 8 became.
|
|
13
|
+
//
|
|
14
|
+
// ⚠️ It touches `document` and `window` ON PURPOSE and that is why it is `platform/`: this is the layer whose job is the
|
|
15
|
+
// browser. What it must never do is decide anything about language — it receives the locale and the tag already chosen.
|
|
16
|
+
/**
|
|
17
|
+
* What a browser can offer a translator: the page effects of a change, the language the person's browser is set to, and
|
|
18
|
+
* the page's signal that ANOTHER root changed the language (ADR-0232 D3: the language is the page's, and every root follows).
|
|
19
|
+
*/
|
|
20
|
+
export function localeHostHooks(doc, win, applyDom) {
|
|
21
|
+
return {
|
|
22
|
+
applied: (locale, tag) => {
|
|
23
|
+
// 📌 THE THREE EFFECTS IN THE ORDER THEY WERE WRITTEN IN `core/i18n`, and the order matters to a screen reader: the
|
|
24
|
+
// document's language is announced before the text it governs changes.
|
|
25
|
+
doc.documentElement.lang = tag;
|
|
26
|
+
applyDom(doc);
|
|
27
|
+
win.dispatchEvent(new CustomEvent('i18n:change', { detail: { locale } }));
|
|
28
|
+
},
|
|
29
|
+
// ⚠️ `navigator` through the window, not the global: a host that hands the engine another window — a second document, an
|
|
30
|
+
// iframe — means that window's language, and reading the global would silently answer for the wrong one.
|
|
31
|
+
preferred: () => win.navigator?.language ?? null,
|
|
32
|
+
// 📌 On the window the root was GIVEN — its listener scope, so a disposed root stops following (ADR-0220).
|
|
33
|
+
follow: (react) => {
|
|
34
|
+
win.addEventListener('i18n:change', (e) => {
|
|
35
|
+
const locale = e.detail?.locale;
|
|
36
|
+
if (typeof locale === 'string')
|
|
37
|
+
react(locale);
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Hangs a root's translator on the window for tests and the preview, which is what `core/i18n` used to do to itself. */
|
|
43
|
+
export function exposeI18n(win, i18n) {
|
|
44
|
+
win.__i18n = i18n;
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
/** The part of the Web Audio API this uses — narrow on purpose, so a case can answer it without a browser. */
|
|
3
|
+
export interface AudioNodeLike {
|
|
4
|
+
connect(to: unknown): void;
|
|
5
|
+
disconnect(): void;
|
|
6
|
+
}
|
|
7
|
+
export interface ProcessorNode extends AudioNodeLike {
|
|
8
|
+
onaudioprocess: ((e: {
|
|
9
|
+
inputBuffer: {
|
|
10
|
+
getChannelData(ch: number): Float32Array;
|
|
11
|
+
};
|
|
12
|
+
}) => void) | null;
|
|
13
|
+
}
|
|
14
|
+
export interface AudioContextLike {
|
|
15
|
+
readonly sampleRate: number;
|
|
16
|
+
readonly destination: unknown;
|
|
17
|
+
createMediaStreamSource(stream: unknown): AudioNodeLike;
|
|
18
|
+
createScriptProcessor(bufferSize: number, inputs: number, outputs: number): ProcessorNode;
|
|
19
|
+
close(): Promise<void> | void;
|
|
20
|
+
}
|
|
21
|
+
/** A stream, in the one thing that matters after the reading: letting go of it. */
|
|
22
|
+
export interface StreamLike {
|
|
23
|
+
getTracks(): readonly {
|
|
24
|
+
stop(): void;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
/** What the microphone uses of the browser, lent by the root from the host's window (ADR-0232 D4). All REQUIRED. */
|
|
28
|
+
export interface MicrophoneDeps {
|
|
29
|
+
/** The host's `navigator.mediaDevices.getUserMedia`. `undefined` is a device with no microphone: REFUSED and said, not hidden. */
|
|
30
|
+
readonly getUserMedia: ((constraints: {
|
|
31
|
+
audio: boolean;
|
|
32
|
+
}) => Promise<StreamLike>) | undefined;
|
|
33
|
+
/** A new audio context at this rate, from the host's `AudioContext`. */
|
|
34
|
+
readonly createContext: (rate: number) => AudioContextLike;
|
|
35
|
+
/** The host's clock, `performance.now()`: what the silence and the ceiling are measured against. */
|
|
36
|
+
readonly now: () => number;
|
|
37
|
+
}
|
|
38
|
+
export interface RecordOptions {
|
|
39
|
+
/** How long a silence ends the reading. Default 1.5 s: shorter cuts a child who is thinking between words. */
|
|
40
|
+
readonly silenceMs?: number;
|
|
41
|
+
/** The ceiling, so a forgotten microphone does not listen for ever. Default 30 s, the window Whisper reads at once. */
|
|
42
|
+
readonly maxMs?: number;
|
|
43
|
+
}
|
|
44
|
+
export interface Microphone {
|
|
45
|
+
/** Listens, and answers with what was said, at 16 kHz mono. */
|
|
46
|
+
record(options?: RecordOptions): Promise<Float32Array>;
|
|
47
|
+
/** Gives the microphone back now; the recording in hand answers with what it has. */
|
|
48
|
+
stop(): void;
|
|
49
|
+
}
|
|
50
|
+
export declare const READING_RATE = 16000;
|
|
51
|
+
export declare function createMicrophone(d: MicrophoneDeps): Microphone;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// platform/microphone — THE CHILD'S VOICE, AND NOTHING ELSE (ADR-0216 §2, ADR-0200 erratum; issues #185, #200).
|
|
3
|
+
//
|
|
4
|
+
// The reading model eats 16 kHz mono samples. This is where they come from, and it is the only place in the engine that opens a
|
|
5
|
+
// microphone for reading: what it captures stays in this page, is handed to the model on this machine, and is dropped when the
|
|
6
|
+
// reading ends (ADR-0200's erratum: on the device or not at all).
|
|
7
|
+
//
|
|
8
|
+
// · IT ENDS WHEN THE CHILD DOES. A reading closes after `silenceMs` of quiet FOLLOWING something that was said, or at the `maxMs`
|
|
9
|
+
// ceiling, whichever comes first. A child who stops mid-sentence is not an error: what was heard so far is the answer.
|
|
10
|
+
// · «QUIET» IS MEASURED, NOT ASSUMED. The first moments are taken as the room she is in, and sound is what stands above it —
|
|
11
|
+
// a fixed threshold would end a whispering child's reading in a noisy classroom, and never end in a quiet one.
|
|
12
|
+
// · ⚠️ IT USES `ScriptProcessorNode`, which is deprecated. The replacement, an AudioWorklet, needs its code fetched as a module,
|
|
13
|
+
// and the engine's CSP has no `blob:` in `script-src` since ADR-0214 — so the modern way would need a new file in every
|
|
14
|
+
// delivery and a decision about serving it. This one works everywhere the project targets today, and it is written here so the
|
|
15
|
+
// next person does not «fix» it without that decision.
|
|
16
|
+
export const READING_RATE = 16_000;
|
|
17
|
+
const SILENCE_MS = 1500;
|
|
18
|
+
const MAX_MS = 30_000;
|
|
19
|
+
const BUFFER = 4096;
|
|
20
|
+
/** How much of the start is taken as «the room»: long enough to be a room, short enough not to swallow a first word. */
|
|
21
|
+
const ROOM_MS = 300;
|
|
22
|
+
/** How far above the room a sound must be to be a voice, and the floor under it for a digitally silent input. */
|
|
23
|
+
const OVER_ROOM = 4;
|
|
24
|
+
const QUIETEST = 0.005;
|
|
25
|
+
const rms = (block) => {
|
|
26
|
+
let sum = 0;
|
|
27
|
+
for (const v of block)
|
|
28
|
+
sum += v * v;
|
|
29
|
+
return Math.sqrt(sum / (block.length || 1));
|
|
30
|
+
};
|
|
31
|
+
/** 16 kHz from whatever the device gave us. Linear, because what follows is a mel filterbank, not a listener. */
|
|
32
|
+
function atReadingRate(samples, from) {
|
|
33
|
+
if (from === READING_RATE)
|
|
34
|
+
return samples;
|
|
35
|
+
const out = new Float32Array(Math.round((samples.length * READING_RATE) / from));
|
|
36
|
+
const step = from / READING_RATE;
|
|
37
|
+
for (let i = 0; i < out.length; i++) {
|
|
38
|
+
const at = i * step;
|
|
39
|
+
const left = Math.floor(at);
|
|
40
|
+
const right = Math.min(left + 1, samples.length - 1);
|
|
41
|
+
out[i] = samples[left] + (samples[right] - samples[left]) * (at - left);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
export function createMicrophone(d) {
|
|
46
|
+
const { now } = d;
|
|
47
|
+
let askToStop = null;
|
|
48
|
+
return {
|
|
49
|
+
stop() { askToStop?.(); },
|
|
50
|
+
async record(options = {}) {
|
|
51
|
+
const { getUserMedia } = d;
|
|
52
|
+
if (!getUserMedia)
|
|
53
|
+
throw new Error('microphone: this device offers none, so a reading cannot be heard here');
|
|
54
|
+
const makeContext = d.createContext;
|
|
55
|
+
const silenceMs = options.silenceMs ?? SILENCE_MS;
|
|
56
|
+
const maxMs = options.maxMs ?? MAX_MS;
|
|
57
|
+
const stream = await getUserMedia({ audio: true });
|
|
58
|
+
const context = makeContext(READING_RATE);
|
|
59
|
+
const source = context.createMediaStreamSource(stream);
|
|
60
|
+
const processor = context.createScriptProcessor(BUFFER, 1, 1);
|
|
61
|
+
const blocks = [];
|
|
62
|
+
const startedAt = now();
|
|
63
|
+
let room = 0, roomBlocks = 0, spoke = false, lastSound = startedAt, done = false;
|
|
64
|
+
return await new Promise((resolve) => {
|
|
65
|
+
const finish = () => {
|
|
66
|
+
if (done)
|
|
67
|
+
return;
|
|
68
|
+
done = true;
|
|
69
|
+
askToStop = null;
|
|
70
|
+
processor.onaudioprocess = null;
|
|
71
|
+
processor.disconnect();
|
|
72
|
+
source.disconnect();
|
|
73
|
+
// ⚠️ THE LIGHT GOES OUT. A track left running keeps the microphone open and the browser saying so, which to a child
|
|
74
|
+
// and to whoever is with her means the game is still listening.
|
|
75
|
+
for (const track of stream.getTracks())
|
|
76
|
+
track.stop();
|
|
77
|
+
void context.close();
|
|
78
|
+
let total = 0;
|
|
79
|
+
for (const b of blocks)
|
|
80
|
+
total += b.length;
|
|
81
|
+
const whole = new Float32Array(total);
|
|
82
|
+
let at = 0;
|
|
83
|
+
for (const b of blocks) {
|
|
84
|
+
whole.set(b, at);
|
|
85
|
+
at += b.length;
|
|
86
|
+
}
|
|
87
|
+
resolve(atReadingRate(whole, context.sampleRate));
|
|
88
|
+
};
|
|
89
|
+
askToStop = finish;
|
|
90
|
+
processor.onaudioprocess = (e) => {
|
|
91
|
+
if (done)
|
|
92
|
+
return;
|
|
93
|
+
const block = Float32Array.from(e.inputBuffer.getChannelData(0));
|
|
94
|
+
blocks.push(block);
|
|
95
|
+
const level = rms(block);
|
|
96
|
+
const elapsed = now() - startedAt;
|
|
97
|
+
if (elapsed < ROOM_MS) {
|
|
98
|
+
room = (room * roomBlocks + level) / (roomBlocks + 1);
|
|
99
|
+
roomBlocks++;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (level > Math.max(room * OVER_ROOM, QUIETEST)) {
|
|
103
|
+
spoke = true;
|
|
104
|
+
lastSound = now();
|
|
105
|
+
}
|
|
106
|
+
else if (spoke && now() - lastSound >= silenceMs) {
|
|
107
|
+
finish();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (elapsed >= maxMs)
|
|
111
|
+
finish();
|
|
112
|
+
};
|
|
113
|
+
source.connect(processor);
|
|
114
|
+
processor.connect(context.destination);
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
export interface OnnxTensor {
|
|
3
|
+
readonly data: unknown;
|
|
4
|
+
readonly dims?: readonly number[];
|
|
5
|
+
}
|
|
6
|
+
/** An open graph: the names it answers with, and one run. */
|
|
7
|
+
export interface OnnxSession {
|
|
8
|
+
readonly inputNames?: readonly string[];
|
|
9
|
+
readonly outputNames: readonly string[];
|
|
10
|
+
run(inputs: {
|
|
11
|
+
readonly [name: string]: OnnxTensor;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
readonly [name: string]: OnnxTensor;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
/** The part of onnxruntime the engine uses — small on purpose: what is not named here cannot be depended on. */
|
|
17
|
+
export interface OnnxRuntime {
|
|
18
|
+
readonly Tensor: new (type: 'int64' | 'float32' | 'bool', data: BigInt64Array | Float32Array | Uint8Array, shape: readonly number[]) => OnnxTensor;
|
|
19
|
+
readonly InferenceSession: {
|
|
20
|
+
create(model: Uint8Array, options: {
|
|
21
|
+
executionProviders: readonly string[];
|
|
22
|
+
}): Promise<OnnxSession>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Where the delivery serves a catalogued file, from the page's own address. */
|
|
26
|
+
export declare const atDelivery: (id: string, base: string) => string;
|
|
27
|
+
export interface OnnxRuntimeDeps {
|
|
28
|
+
/** The page's address, which `heavy/` is resolved against. */
|
|
29
|
+
readonly base: string;
|
|
30
|
+
/** `import()`, injected so a case can hand in a module without a network or a wasm engine. */
|
|
31
|
+
readonly importModule?: (url: string) => Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
/** The runtime, from the delivery, with its threads pointed at the delivery too. Refuses by NAME when it is not there. */
|
|
34
|
+
export declare function loadOnnxRuntime(d: OnnxRuntimeDeps): Promise<OnnxRuntime>;
|