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